From b39fab45518b7bd0fcf8da1bdd3107b02361b2a2 Mon Sep 17 00:00:00 2001 From: Tu Shaokun <2801884530@qq.com> Date: Mon, 15 Dec 2025 22:25:53 +0800 Subject: [PATCH 1/8] feat(linter): implement eslint/camelcase rule Implements the `camelcase` rule from ESLint that enforces camelCase naming conventions for identifiers. Supports options: - `properties`: "always" (default) or "never" - `ignoreDestructuring`: skip destructuring patterns - `ignoreImports`: skip import specifiers - `allow`: array of allowed names or regex patterns Checks: - Variable declarations - Function declarations and parameters - Object property definitions - Class properties and methods - Assignment expressions - Import/export declarations - Labels --- .../src/generated/rule_runner_impls.rs | 22 + crates/oxc_linter/src/rules.rs | 2 + .../oxc_linter/src/rules/eslint/camelcase.rs | 508 ++++++++++++++++++ .../src/snapshots/eslint_camelcase.snap | 240 +++++++++ 4 files changed, 772 insertions(+) create mode 100644 crates/oxc_linter/src/rules/eslint/camelcase.rs create mode 100644 crates/oxc_linter/src/snapshots/eslint_camelcase.snap diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index db4a4955210ab..a2bc3f4b1389d 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -37,6 +37,28 @@ impl RuleRunner for crate::rules::eslint::block_scoped_var::BlockScopedVar { const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } +impl RuleRunner for crate::rules::eslint::camelcase::Camelcase { + const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ + AstType::AssignmentExpression, + AstType::AssignmentTargetPropertyIdentifier, + AstType::AssignmentTargetPropertyProperty, + AstType::BindingProperty, + AstType::ExportAllDeclaration, + AstType::FormalParameter, + AstType::Function, + AstType::ImportDefaultSpecifier, + AstType::ImportNamespaceSpecifier, + AstType::ImportSpecifier, + AstType::LabeledStatement, + AstType::MethodDefinition, + AstType::ObjectProperty, + AstType::PrivateIdentifier, + AstType::PropertyDefinition, + AstType::VariableDeclarator, + ])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::eslint::class_methods_use_this::ClassMethodsUseThis { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ AstType::AccessorProperty, diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 9c480ff4973f6..999bf6ed2e622 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -44,6 +44,7 @@ pub(crate) mod eslint { pub mod array_callback_return; pub mod arrow_body_style; pub mod block_scoped_var; + pub mod camelcase; pub mod class_methods_use_this; pub mod constructor_super; pub mod curly; @@ -688,6 +689,7 @@ oxc_macros::declare_all_lint_rules! { eslint::array_callback_return, eslint::arrow_body_style, eslint::block_scoped_var, + eslint::camelcase, eslint::class_methods_use_this, eslint::constructor_super, eslint::curly, diff --git a/crates/oxc_linter/src/rules/eslint/camelcase.rs b/crates/oxc_linter/src/rules/eslint/camelcase.rs new file mode 100644 index 0000000000000..76f4978e6f7f9 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/camelcase.rs @@ -0,0 +1,508 @@ +use lazy_regex::Regex; +use oxc_ast::AstKind; +use oxc_ast::ast::{AssignmentTarget, BindingPatternKind, Expression, PropertyKey}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::{AstNode, context::LintContext, rule::Rule}; + +fn camelcase_diagnostic(name: &str, span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("Identifier '{name}' is not in camel case.")) + .with_help("Rename this identifier to use camelCase.") + .with_label(span) +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +enum PropertiesOption { + #[default] + Always, + Never, +} + +/// Pre-compiled allow pattern (either literal string or regex) +#[derive(Debug, Clone)] +enum AllowPattern { + Literal(String), + Regex(Regex), +} + +impl AllowPattern { + fn matches(&self, name: &str) -> bool { + match self { + AllowPattern::Literal(s) => s == name, + AllowPattern::Regex(re) => re.is_match(name), + } + } +} + +#[derive(Debug, Default, Clone, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct CamelcaseConfig { + /// When set to "never", the rule will not check property names. + properties: PropertiesOption, + /// When set to true, the rule will not check destructuring identifiers. + ignore_destructuring: bool, + /// When set to true, the rule will not check import identifiers. + ignore_imports: bool, + /// An array of names or regex patterns to allow. + /// Patterns starting with `^` or ending with `$` are treated as regular expressions. + #[serde(default)] + allow: Vec, +} + +/// Runtime configuration with pre-compiled patterns +#[derive(Debug, Clone, Default)] +struct CamelcaseRuntime { + properties: PropertiesOption, + ignore_destructuring: bool, + ignore_imports: bool, + allow_patterns: Vec, +} + +impl From for CamelcaseRuntime { + fn from(config: CamelcaseConfig) -> Self { + let allow_patterns = config + .allow + .into_iter() + .map(|pattern| { + if pattern.starts_with('^') || pattern.ends_with('$') { + Regex::new(&pattern) + .map_or_else(|_| AllowPattern::Literal(pattern), AllowPattern::Regex) + } else { + AllowPattern::Literal(pattern) + } + }) + .collect(); + + Self { + properties: config.properties, + ignore_destructuring: config.ignore_destructuring, + ignore_imports: config.ignore_imports, + allow_patterns, + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct Camelcase(Box); + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforces camelCase naming convention. + /// + /// ### Why is this bad? + /// + /// Inconsistent naming conventions make code harder to read and maintain. + /// The camelCase convention is widely used in JavaScript and helps maintain + /// a consistent codebase. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// var my_variable = 1; + /// function do_something() {} + /// obj.my_prop = 2; + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// var myVariable = 1; + /// function doSomething() {} + /// obj.myProp = 2; + /// var CONSTANT_VALUE = 1; // all caps allowed + /// var _privateVar = 1; // leading underscore allowed + /// ``` + Camelcase, + eslint, + style, + config = CamelcaseConfig, +); + +impl Rule for Camelcase { + fn from_configuration(value: serde_json::Value) -> Self { + let config: CamelcaseConfig = + value.get(0).and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default(); + Self(Box::new(config.into())) + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + // Variable declarations: var foo_bar = 1; + AstKind::VariableDeclarator(decl) => { + // Only check simple binding identifiers, not destructuring patterns + // Destructuring (ObjectPattern/ArrayPattern) is handled by BindingProperty + if let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind { + self.check_name(&ident.name, ident.span, ctx); + } + } + + // Destructuring patterns in variable declarations + AstKind::BindingProperty(prop) => { + // Skip if ignoreDestructuring is enabled + if self.0.ignore_destructuring { + return; + } + + // Check the value (the local binding name) + match &prop.value.kind { + BindingPatternKind::BindingIdentifier(ident) => { + self.check_name(&ident.name, ident.span, ctx); + } + // Handle destructuring with default value: { category_id = 1 } + BindingPatternKind::AssignmentPattern(pattern) => { + if let BindingPatternKind::BindingIdentifier(ident) = &pattern.left.kind { + self.check_name(&ident.name, ident.span, ctx); + } + } + _ => {} + } + } + + // Function declarations: function foo_bar() {} + AstKind::Function(func) => { + if let Some(ident) = &func.id { + self.check_name(&ident.name, ident.span, ctx); + } + } + + // Function/method parameters + AstKind::FormalParameter(param) => { + if let BindingPatternKind::BindingIdentifier(ident) = ¶m.pattern.kind { + self.check_name(&ident.name, ident.span, ctx); + } + } + + // Object property definitions: { foo_bar: 1 } + AstKind::ObjectProperty(prop) => { + if self.0.properties == PropertiesOption::Never { + return; + } + + // Check property key if it's an identifier + if let PropertyKey::StaticIdentifier(ident) = &prop.key { + self.check_name(&ident.name, ident.span, ctx); + } + } + + // Class property definitions + AstKind::PropertyDefinition(prop) => { + if self.0.properties == PropertiesOption::Never { + return; + } + + if let PropertyKey::StaticIdentifier(ident) = &prop.key { + self.check_name(&ident.name, ident.span, ctx); + } + } + + // Private identifiers in classes: #foo_bar + AstKind::PrivateIdentifier(ident) => { + if self.0.properties == PropertiesOption::Never { + return; + } + self.check_name(&ident.name, ident.span, ctx); + } + + // Method definitions + AstKind::MethodDefinition(method) => { + if self.0.properties == PropertiesOption::Never { + return; + } + + if let PropertyKey::StaticIdentifier(ident) = &method.key { + self.check_name(&ident.name, ident.span, ctx); + } + } + + // Assignment expressions + AstKind::AssignmentExpression(assign) => { + match &assign.left { + // Simple identifier assignment: foo_bar = 1 + AssignmentTarget::AssignmentTargetIdentifier(ident) => { + self.check_name(&ident.name, ident.span, ctx); + } + // Member expression assignment: obj.foo_bar = 1 or bar_baz.foo = 1 + AssignmentTarget::StaticMemberExpression(member) => { + // Check the object identifier (bar_baz in bar_baz.foo) + if let Expression::Identifier(obj_ident) = &member.object { + self.check_name(&obj_ident.name, obj_ident.span, ctx); + } + // Check the property if properties option is "always" + if self.0.properties != PropertiesOption::Never { + self.check_name(&member.property.name, member.property.span, ctx); + } + } + _ => {} + } + } + + // Import declarations + AstKind::ImportSpecifier(specifier) => { + if self.0.ignore_imports { + return; + } + + // Always check the local name (the name used in current scope) + self.check_name(&specifier.local.name, specifier.local.span, ctx); + } + + AstKind::ImportDefaultSpecifier(specifier) => { + if self.0.ignore_imports { + return; + } + self.check_name(&specifier.local.name, specifier.local.span, ctx); + } + + AstKind::ImportNamespaceSpecifier(specifier) => { + if self.0.ignore_imports { + return; + } + self.check_name(&specifier.local.name, specifier.local.span, ctx); + } + + // Export all: export * as foo_bar from 'mod' + AstKind::ExportAllDeclaration(export) => { + if let Some(exported) = &export.exported + && let Some(name) = exported.identifier_name() + { + self.check_name(name.as_str(), exported.span(), ctx); + } + } + + // Destructuring assignment (not declaration): ({ foo_bar } = obj) + // For shorthand: { foo_bar } = obj + AstKind::AssignmentTargetPropertyIdentifier(ident) => { + if self.0.ignore_destructuring { + return; + } + self.check_name(&ident.binding.name, ident.binding.span, ctx); + } + + // For renamed: { key: foo_bar } = obj - check foo_bar + AstKind::AssignmentTargetPropertyProperty(prop) => { + if self.0.ignore_destructuring { + return; + } + // Check the binding target if it's a simple identifier + if let oxc_ast::ast::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier( + ident, + ) = &prop.binding + { + self.check_name(&ident.name, ident.span, ctx); + } + } + + // Labels + AstKind::LabeledStatement(stmt) => { + self.check_name(&stmt.label.name, stmt.label.span, ctx); + } + + _ => {} + } + } +} + +impl Camelcase { + /// Check if a name violates the camelCase rule + fn check_name(&self, name: &str, span: Span, ctx: &LintContext) { + if self.is_good_name(name) { + return; + } + ctx.diagnostic(camelcase_diagnostic(name, span)); + } + + /// Check if a name is acceptable (either camelCase or in the allow list) + fn is_good_name(&self, name: &str) -> bool { + // Check pre-compiled allow patterns first + if self.0.allow_patterns.iter().any(|p| p.matches(name)) { + return true; + } + + !is_underscored(name) + } +} + +/// Check if a name contains underscores in the middle (not camelCase). +/// Leading and trailing underscores are allowed. +/// ALL_CAPS names (constants) are allowed. +fn is_underscored(name: &str) -> bool { + // Strip leading underscores + let name = name.trim_start_matches('_'); + // Strip trailing underscores + let name = name.trim_end_matches('_'); + + // Empty string or single char after stripping is fine + if name.is_empty() { + return false; + } + + // Check if it's ALL_CAPS (constant style) - these are allowed + if is_all_caps(name) { + return false; + } + + // Check for underscore in the middle + name.contains('_') +} + +/// Check if a name is in ALL_CAPS style (allowed for constants) +fn is_all_caps(name: &str) -> bool { + // Must contain at least one letter + let has_letter = name.chars().any(char::is_alphabetic); + if !has_letter { + return false; + } + + // All letters must be uppercase, and underscores/digits are allowed + name.chars().all(|c| c.is_uppercase() || c.is_ascii_digit() || c == '_') +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + (r#"firstName = "Nicholas""#, None), + (r#"FIRST_NAME = "Nicholas""#, None), + (r#"__myPrivateVariable = "Patrick""#, None), + (r#"myPrivateVariable_ = "Patrick""#, None), + ("function doSomething(){}", None), + ("do_something()", None), + ("new do_something", None), + ("new do_something()", None), + ("foo.do_something()", None), + ("var foo = bar.baz_boom;", None), + ("var foo = bar.baz_boom.something;", None), + ("foo.boom_pow.qux = bar.baz_boom.something;", None), + ("if (bar.baz_boom) {}", None), + ("var obj = { key: foo.bar_baz };", None), + ("var arr = [foo.bar_baz];", None), + ("[foo.bar_baz]", None), + ("var arr = [foo.bar_baz.qux];", None), + ("[foo.bar_baz.nesting]", None), + ("if (foo.bar_baz === boom.bam_pow) { [foo.baz_boom] }", None), + ("var o = {key: 1}", Some(serde_json::json!([{ "properties": "always" }]))), + ("var o = {_leading: 1}", Some(serde_json::json!([{ "properties": "always" }]))), + ("var o = {trailing_: 1}", Some(serde_json::json!([{ "properties": "always" }]))), + ("var o = {bar_baz: 1}", Some(serde_json::json!([{ "properties": "never" }]))), + ("var o = {_leading: 1}", Some(serde_json::json!([{ "properties": "never" }]))), + ("var o = {trailing_: 1}", Some(serde_json::json!([{ "properties": "never" }]))), + ("obj.a_b = 2;", Some(serde_json::json!([{ "properties": "never" }]))), + ("obj._a = 2;", Some(serde_json::json!([{ "properties": "always" }]))), + ("obj.a_ = 2;", Some(serde_json::json!([{ "properties": "always" }]))), + ("obj._a = 2;", Some(serde_json::json!([{ "properties": "never" }]))), + ("obj.a_ = 2;", Some(serde_json::json!([{ "properties": "never" }]))), + ( + "var obj = { + a_a: 1 + }; + obj.a_b = 2;", + Some(serde_json::json!([{ "properties": "never" }])), + ), + ("obj.foo_bar = function(){};", Some(serde_json::json!([{ "properties": "never" }]))), + ("const { ['foo']: _foo } = obj;", None), + ("const { [_foo_]: foo } = obj;", None), + ( + "var { category_id } = query;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + ( + "var { category_id: category_id } = query;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + ( + "var { category_id = 1 } = query;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + ("var { category_id: category } = query;", None), + ("var { _leading } = query;", None), + ("var { trailing_ } = query;", None), + (r#"import { camelCased } from "external module";"#, None), + (r#"import { _leading } from "external module";"#, None), + (r#"import { trailing_ } from "external module";"#, None), + (r#"import { no_camelcased as camelCased } from "external-module";"#, None), + (r#"import { no_camelcased as _leading } from "external-module";"#, None), + (r#"import { no_camelcased as trailing_ } from "external-module";"#, None), + ( + r#"import { no_camelcased as camelCased, anotherCamelCased } from "external-module";"#, + None, + ), + ("import { snake_cased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }]))), + ("function foo({ no_camelcased: camelCased }) {};", None), + ("function foo({ no_camelcased: _leading }) {};", None), + ("function foo({ no_camelcased: trailing_ }) {};", None), + ("function foo({ camelCased = 'default value' }) {};", None), + ("function foo({ _leading = 'default value' }) {};", None), + ("function foo({ trailing_ = 'default value' }) {};", None), + ("function foo({ camelCased }) {};", None), + ("function foo({ _leading }) {}", None), + ("function foo({ trailing_ }) {}", None), + ("ignored_foo = 0;", Some(serde_json::json!([{ "allow": ["ignored_foo"] }]))), + ( + "ignored_foo = 0; ignored_bar = 1;", + Some(serde_json::json!([{ "allow": ["ignored_foo", "ignored_bar"] }])), + ), + ("user_id = 0;", Some(serde_json::json!([{ "allow": ["_id$"] }]))), + ("__option_foo__ = 0;", Some(serde_json::json!([{ "allow": ["__option_foo__"] }]))), + ( + "class C { camelCase; #camelCase; #camelCase2() {} }", + Some(serde_json::json!([{ "properties": "always" }])), + ), + ( + "class C { snake_case; #snake_case; #snake_case2() {} }", + Some(serde_json::json!([{ "properties": "never" }])), + ), + // ignoreDestructuring applies to destructuring assignments too + ("({ foo_bar } = obj);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), + ("({ key: bar_baz } = obj);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), + ]; + + let fail = vec![ + (r#"first_name = "Nicholas""#, None), + (r#"__private_first_name = "Patrick""#, None), + ("function foo_bar(){}", None), + ("obj.foo_bar = function(){};", None), + ("bar_baz.foo = function(){};", None), + ("var foo = { bar_baz: boom.bam_pow }", None), + ("var o = {bar_baz: 1}", Some(serde_json::json!([{ "properties": "always" }]))), + ("obj.a_b = 2;", Some(serde_json::json!([{ "properties": "always" }]))), + ("var { category_id } = query;", None), + ("var { category_id: category_id } = query;", None), + ("var { category_id = 1 } = query;", None), + (r#"import no_camelcased from "external-module";"#, None), + (r#"import * as no_camelcased from "external-module";"#, None), + (r#"import { no_camelcased } from "external-module";"#, None), + (r#"import { no_camelcased as no_camel_cased } from "external module";"#, None), + (r#"import { camelCased as no_camel_cased } from "external module";"#, None), + (r#"import { camelCased, no_camelcased } from "external-module";"#, None), + ("export * as snake_cased from 'mod'", None), + ("function foo({ no_camelcased }) {};", None), + ("function foo({ no_camelcased = 'default value' }) {};", None), + ("const { bar: no_camelcased } = foo;", None), + ("function foo({ value_1: my_default }) {}", None), + ("function foo({ isCamelcased: no_camelcased }) {};", None), + ("var { foo: bar_baz = 1 } = quz;", None), + ("const { no_camelcased = false } = bar;", None), + ("not_ignored_foo = 0;", Some(serde_json::json!([{ "allow": ["ignored_bar"] }]))), + ("class C { snake_case; }", Some(serde_json::json!([{ "properties": "always" }]))), + ( + "class C { #snake_case; foo() { this.#snake_case; } }", + Some(serde_json::json!([{ "properties": "always" }])), + ), + ("class C { #snake_case() {} }", Some(serde_json::json!([{ "properties": "always" }]))), + // Ensure var foo_bar = {} is NOT mistaken for destructuring + ("var foo_bar = {};", None), + ("var foo_bar = [];", None), + // Destructuring assignments (not declarations) + ("({ foo_bar } = obj);", None), + ("({ key: bar_baz } = obj);", None), + ]; + + Tester::new(Camelcase::NAME, Camelcase::PLUGIN, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/eslint_camelcase.snap b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap new file mode 100644 index 0000000000000..83e59bc30dc5c --- /dev/null +++ b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap @@ -0,0 +1,240 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint(camelcase): Identifier 'first_name' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ first_name = "Nicholas" + · ────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier '__private_first_name' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ __private_first_name = "Patrick" + · ──────────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:10] + 1 │ function foo_bar(){} + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:5] + 1 │ obj.foo_bar = function(){}; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ bar_baz.foo = function(){}; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ var foo = { bar_baz: boom.bam_pow } + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. + ╭─[camelcase.tsx:1:10] + 1 │ var o = {bar_baz: 1} + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_b' is not in camel case. + ╭─[camelcase.tsx:1:5] + 1 │ obj.a_b = 2; + · ─── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'category_id' is not in camel case. + ╭─[camelcase.tsx:1:7] + 1 │ var { category_id } = query; + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'category_id' is not in camel case. + ╭─[camelcase.tsx:1:20] + 1 │ var { category_id: category_id } = query; + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'category_id' is not in camel case. + ╭─[camelcase.tsx:1:7] + 1 │ var { category_id = 1 } = query; + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ import no_camelcased from "external-module"; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ import * as no_camelcased from "external-module"; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:10] + 1 │ import { no_camelcased } from "external-module"; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camel_cased' is not in camel case. + ╭─[camelcase.tsx:1:27] + 1 │ import { no_camelcased as no_camel_cased } from "external module"; + · ────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camel_cased' is not in camel case. + ╭─[camelcase.tsx:1:24] + 1 │ import { camelCased as no_camel_cased } from "external module"; + · ────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:22] + 1 │ import { camelCased, no_camelcased } from "external-module"; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ export * as snake_cased from 'mod' + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:16] + 1 │ function foo({ no_camelcased }) {}; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:16] + 1 │ function foo({ no_camelcased = 'default value' }) {}; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:14] + 1 │ const { bar: no_camelcased } = foo; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'my_default' is not in camel case. + ╭─[camelcase.tsx:1:25] + 1 │ function foo({ value_1: my_default }) {} + · ────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:30] + 1 │ function foo({ isCamelcased: no_camelcased }) {}; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. + ╭─[camelcase.tsx:1:12] + 1 │ var { foo: bar_baz = 1 } = quz; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:9] + 1 │ const { no_camelcased = false } = bar; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'not_ignored_foo' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ not_ignored_foo = 0; + · ─────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. + ╭─[camelcase.tsx:1:11] + 1 │ class C { snake_case; } + · ────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. + ╭─[camelcase.tsx:1:11] + 1 │ class C { #snake_case; foo() { this.#snake_case; } } + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. + ╭─[camelcase.tsx:1:37] + 1 │ class C { #snake_case; foo() { this.#snake_case; } } + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. + ╭─[camelcase.tsx:1:11] + 1 │ class C { #snake_case() {} } + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:5] + 1 │ var foo_bar = {}; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:5] + 1 │ var foo_bar = []; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:4] + 1 │ ({ foo_bar } = obj); + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. + ╭─[camelcase.tsx:1:9] + 1 │ ({ key: bar_baz } = obj); + · ─────── + ╰──── + help: Rename this identifier to use camelCase. From c13e2c7c5238b61cb040bb3309526fd9e4d76f57 Mon Sep 17 00:00:00 2001 From: Tu Shaokun <2801884530@qq.com> Date: Tue, 16 Dec 2025 09:41:56 +0800 Subject: [PATCH 2/8] fix(linter): improve eslint/camelcase ESLint compatibility - ignoreDestructuring: only skip when local name equals property name (e.g., { category_id } skipped, but { category_id: other } checked) - ignoreImports: only skip when local equals imported name (e.g., import { snake } skipped, but import { snake as other } checked) - allow: treat each entry as both literal and regex (ESLint behavior) - Add ExportSpecifier check for named exports - Add BreakStatement/ContinueStatement label reference checks --- .../src/generated/rule_runner_impls.rs | 3 + .../oxc_linter/src/rules/eslint/camelcase.rs | 182 ++++++++++++++---- .../src/snapshots/eslint_camelcase.snap | 21 ++ 3 files changed, 169 insertions(+), 37 deletions(-) diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index a2bc3f4b1389d..d272277675f6b 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -43,7 +43,10 @@ impl RuleRunner for crate::rules::eslint::camelcase::Camelcase { AstType::AssignmentTargetPropertyIdentifier, AstType::AssignmentTargetPropertyProperty, AstType::BindingProperty, + AstType::BreakStatement, + AstType::ContinueStatement, AstType::ExportAllDeclaration, + AstType::ExportSpecifier, AstType::FormalParameter, AstType::Function, AstType::ImportDefaultSpecifier, diff --git a/crates/oxc_linter/src/rules/eslint/camelcase.rs b/crates/oxc_linter/src/rules/eslint/camelcase.rs index 76f4978e6f7f9..45f8dda26abfd 100644 --- a/crates/oxc_linter/src/rules/eslint/camelcase.rs +++ b/crates/oxc_linter/src/rules/eslint/camelcase.rs @@ -23,19 +23,33 @@ enum PropertiesOption { Never, } -/// Pre-compiled allow pattern (either literal string or regex) +/// Pre-compiled allow pattern +/// ESLint treats each entry as both a literal string AND a regex pattern: +/// `allow.some(entry => name === entry || name.match(new RegExp(entry, "u")))` #[derive(Debug, Clone)] -enum AllowPattern { - Literal(String), - Regex(Regex), +struct AllowPattern { + literal: String, + regex: Option, } impl AllowPattern { + fn new(pattern: String) -> Self { + // Try to compile as regex (ESLint uses Unicode flag) + let regex = Regex::new(&pattern).ok(); + Self { literal: pattern, regex } + } + fn matches(&self, name: &str) -> bool { - match self { - AllowPattern::Literal(s) => s == name, - AllowPattern::Regex(re) => re.is_match(name), + // ESLint: name === entry || name.match(new RegExp(entry, "u")) + if name == self.literal { + return true; } + if let Some(ref re) = self.regex + && re.is_match(name) + { + return true; + } + false } } @@ -65,18 +79,7 @@ struct CamelcaseRuntime { impl From for CamelcaseRuntime { fn from(config: CamelcaseConfig) -> Self { - let allow_patterns = config - .allow - .into_iter() - .map(|pattern| { - if pattern.starts_with('^') || pattern.ends_with('$') { - Regex::new(&pattern) - .map_or_else(|_| AllowPattern::Literal(pattern), AllowPattern::Regex) - } else { - AllowPattern::Literal(pattern) - } - }) - .collect(); + let allow_patterns = config.allow.into_iter().map(AllowPattern::new).collect(); Self { properties: config.properties, @@ -144,17 +147,41 @@ impl Rule for Camelcase { // Destructuring patterns in variable declarations AstKind::BindingProperty(prop) => { - // Skip if ignoreDestructuring is enabled - if self.0.ignore_destructuring { + // Get the local binding name + let local_name = match &prop.value.kind { + BindingPatternKind::BindingIdentifier(ident) => Some(&ident.name), + BindingPatternKind::AssignmentPattern(pattern) => { + if let BindingPatternKind::BindingIdentifier(ident) = &pattern.left.kind { + Some(&ident.name) + } else { + None + } + } + _ => None, + }; + + let Some(local_name) = local_name else { return; + }; + + // ESLint ignoreDestructuring: only skip when local name equals property name + // e.g., { category_id } or { category_id: category_id } -> skip + // but { category_id: categoryId } -> still check categoryId + if self.0.ignore_destructuring { + let key_name = match &prop.key { + PropertyKey::StaticIdentifier(ident) => Some(ident.name.as_str()), + _ => None, + }; + if key_name == Some(local_name.as_str()) { + return; + } } - // Check the value (the local binding name) + // Check the local binding name match &prop.value.kind { BindingPatternKind::BindingIdentifier(ident) => { self.check_name(&ident.name, ident.span, ctx); } - // Handle destructuring with default value: { category_id = 1 } BindingPatternKind::AssignmentPattern(pattern) => { if let BindingPatternKind::BindingIdentifier(ident) = &pattern.left.kind { self.check_name(&ident.name, ident.span, ctx); @@ -244,25 +271,29 @@ impl Rule for Camelcase { // Import declarations AstKind::ImportSpecifier(specifier) => { + // ESLint ignoreImports: only skip when local name equals imported name + // e.g., import { snake_case } -> skip (local === imported) + // but import { snake_case as local_name } -> still check local_name if self.0.ignore_imports { - return; + let imported_name = specifier.imported.name(); + if specifier.local.name.as_str() == imported_name.as_str() { + return; + } } - // Always check the local name (the name used in current scope) + // Check the local name (the name used in current scope) self.check_name(&specifier.local.name, specifier.local.span, ctx); } + // Default imports: import foo_bar from 'mod' + // No "imported" name to compare, so ignoreImports doesn't apply AstKind::ImportDefaultSpecifier(specifier) => { - if self.0.ignore_imports { - return; - } self.check_name(&specifier.local.name, specifier.local.span, ctx); } + // Namespace imports: import * as foo_bar from 'mod' + // No "imported" name to compare, so ignoreImports doesn't apply AstKind::ImportNamespaceSpecifier(specifier) => { - if self.0.ignore_imports { - return; - } self.check_name(&specifier.local.name, specifier.local.span, ctx); } @@ -275,9 +306,19 @@ impl Rule for Camelcase { } } + // Named exports: export { foo_bar } or export { foo as bar_baz } + AstKind::ExportSpecifier(specifier) => { + // Check the exported name (the name visible to importers) + if let Some(name) = specifier.exported.identifier_name() { + self.check_name(name.as_str(), specifier.exported.span(), ctx); + } + } + // Destructuring assignment (not declaration): ({ foo_bar } = obj) - // For shorthand: { foo_bar } = obj + // For shorthand: { foo_bar } = obj - this is always local === key AstKind::AssignmentTargetPropertyIdentifier(ident) => { + // Shorthand destructuring: local name always equals property name + // ESLint ignoreDestructuring skips this case if self.0.ignore_destructuring { return; } @@ -286,10 +327,33 @@ impl Rule for Camelcase { // For renamed: { key: foo_bar } = obj - check foo_bar AstKind::AssignmentTargetPropertyProperty(prop) => { - if self.0.ignore_destructuring { + // Get the local binding name + let local_name = + if let oxc_ast::ast::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier( + ident, + ) = &prop.binding + { + Some(ident.name.as_str()) + } else { + None + }; + + let Some(local_name) = local_name else { return; + }; + + // ESLint ignoreDestructuring: only skip when local name equals property name + if self.0.ignore_destructuring { + let key_name = match &prop.name { + PropertyKey::StaticIdentifier(ident) => Some(ident.name.as_str()), + _ => None, + }; + if key_name == Some(local_name) { + return; + } } - // Check the binding target if it's a simple identifier + + // Check the binding target if let oxc_ast::ast::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier( ident, ) = &prop.binding @@ -298,11 +362,25 @@ impl Rule for Camelcase { } } - // Labels + // Labels - both definition and references AstKind::LabeledStatement(stmt) => { self.check_name(&stmt.label.name, stmt.label.span, ctx); } + // break label_name; + AstKind::BreakStatement(stmt) => { + if let Some(label) = &stmt.label { + self.check_name(&label.name, label.span, ctx); + } + } + + // continue label_name; + AstKind::ContinueStatement(stmt) => { + if let Some(label) = &stmt.label { + self.check_name(&label.name, label.span, ctx); + } + } + _ => {} } } @@ -458,9 +536,23 @@ fn test() { "class C { snake_case; #snake_case; #snake_case2() {} }", Some(serde_json::json!([{ "properties": "never" }])), ), - // ignoreDestructuring applies to destructuring assignments too + // ignoreDestructuring applies to destructuring assignments too (only shorthand/same-name) ("({ foo_bar } = obj);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), - ("({ key: bar_baz } = obj);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), + // ESLint: ignoreDestructuring only skips when local === property name + // { category_id: camelCase } -> camelCase is valid, so pass + ( + "var { category_id: camelCase } = query;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + // ignoreImports only skips when local === imported (ESLint behavior) + // import { snake_case as camelCase } -> camelCase is valid, so pass + ( + r#"import { snake_case as camelCase } from "mod";"#, + Some(serde_json::json!([{ "ignoreImports": true }])), + ), + // allow patterns work as regex even without ^/$ + ("foo_bar = 0;", Some(serde_json::json!([{ "allow": ["foo.*"] }]))), + ("get_user_id = 0;", Some(serde_json::json!([{ "allow": ["_id"] }]))), ]; let fail = vec![ @@ -502,6 +594,22 @@ fn test() { // Destructuring assignments (not declarations) ("({ foo_bar } = obj);", None), ("({ key: bar_baz } = obj);", None), + // ESLint ignoreDestructuring: renamed destructuring should still report + // { category_id: other_name } -> other_name should be checked (not equal to key) + ( + "var { category_id: other_name } = query;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + ( + "({ key: other_name } = obj);", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + // ESLint ignoreImports: renamed imports should still report + // import { snake_case as other_snake } -> other_snake should be checked + ( + r#"import { snake_case as other_snake } from "mod";"#, + Some(serde_json::json!([{ "ignoreImports": true }])), + ), ]; Tester::new(Camelcase::NAME, Camelcase::PLUGIN, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/eslint_camelcase.snap b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap index 83e59bc30dc5c..0ab0dc174e726 100644 --- a/crates/oxc_linter/src/snapshots/eslint_camelcase.snap +++ b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap @@ -238,3 +238,24 @@ source: crates/oxc_linter/src/tester.rs · ─────── ╰──── help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'other_name' is not in camel case. + ╭─[camelcase.tsx:1:20] + 1 │ var { category_id: other_name } = query; + · ────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'other_name' is not in camel case. + ╭─[camelcase.tsx:1:9] + 1 │ ({ key: other_name } = obj); + · ────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'other_snake' is not in camel case. + ╭─[camelcase.tsx:1:24] + 1 │ import { snake_case as other_snake } from "mod"; + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. From 673294ebd540cd9ccad117e7226a10a2fe1ab58f Mon Sep 17 00:00:00 2001 From: Tu Shaokun <2801884530@qq.com> Date: Tue, 16 Dec 2025 09:59:31 +0800 Subject: [PATCH 3/8] fix(linter): add rest element and array destructuring support for camelcase - Add BindingRestElement check for rest destructuring (e.g., `...other_props`) - Add ArrayPattern check for array destructuring (e.g., `[foo_bar]`) - Both respect ignoreDestructuring option - Handles default values in array destructuring (e.g., `[foo_bar = 1]`) --- .../src/generated/rule_runner_impls.rs | 2 + .../oxc_linter/src/rules/eslint/camelcase.rs | 67 +++++++++++++++++++ .../src/snapshots/eslint_camelcase.snap | 28 ++++++++ 3 files changed, 97 insertions(+) diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index d272277675f6b..367dc321216dd 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -39,10 +39,12 @@ impl RuleRunner for crate::rules::eslint::block_scoped_var::BlockScopedVar { impl RuleRunner for crate::rules::eslint::camelcase::Camelcase { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ + AstType::ArrayPattern, AstType::AssignmentExpression, AstType::AssignmentTargetPropertyIdentifier, AstType::AssignmentTargetPropertyProperty, AstType::BindingProperty, + AstType::BindingRestElement, AstType::BreakStatement, AstType::ContinueStatement, AstType::ExportAllDeclaration, diff --git a/crates/oxc_linter/src/rules/eslint/camelcase.rs b/crates/oxc_linter/src/rules/eslint/camelcase.rs index 45f8dda26abfd..22c82eaa4b7ba 100644 --- a/crates/oxc_linter/src/rules/eslint/camelcase.rs +++ b/crates/oxc_linter/src/rules/eslint/camelcase.rs @@ -191,6 +191,27 @@ impl Rule for Camelcase { } } + // Rest element in destructuring: { ...other_props } or [...rest] + AstKind::BindingRestElement(rest) => { + // Get the binding name from the rest element's argument + if let BindingPatternKind::BindingIdentifier(ident) = &rest.argument.kind { + // Check if we should skip due to ignoreDestructuring + // Rest elements don't have a "property name" to compare, so they're + // only skipped when ignoreDestructuring is true (ESLint behavior) + if self.0.ignore_destructuring { + return; + } + self.check_name(&ident.name, ident.span, ctx); + } + } + + // Array destructuring: const [foo_bar, bar_baz] = arr; + AstKind::ArrayPattern(pattern) => { + for element in pattern.elements.iter().flatten() { + self.check_binding_pattern(element, ctx); + } + } + // Function declarations: function foo_bar() {} AstKind::Function(func) => { if let Some(ident) = &func.id { @@ -395,6 +416,26 @@ impl Camelcase { ctx.diagnostic(camelcase_diagnostic(name, span)); } + /// Check a binding pattern for camelCase violations (used for array destructuring) + fn check_binding_pattern(&self, pattern: &oxc_ast::ast::BindingPattern, ctx: &LintContext) { + match &pattern.kind { + BindingPatternKind::BindingIdentifier(ident) => { + // For array destructuring, there's no "property name" to compare + // so ignoreDestructuring skips all array element bindings + if self.0.ignore_destructuring { + return; + } + self.check_name(&ident.name, ident.span, ctx); + } + BindingPatternKind::AssignmentPattern(assign) => { + // Handle default values: const [foo_bar = 1] = arr; + self.check_binding_pattern(&assign.left, ctx); + } + // ObjectPattern and ArrayPattern are handled by their own AstKind handlers + _ => {} + } + } + /// Check if a name is acceptable (either camelCase or in the allow list) fn is_good_name(&self, name: &str) -> bool { // Check pre-compiled allow patterns first @@ -553,8 +594,25 @@ fn test() { // allow patterns work as regex even without ^/$ ("foo_bar = 0;", Some(serde_json::json!([{ "allow": ["foo.*"] }]))), ("get_user_id = 0;", Some(serde_json::json!([{ "allow": ["_id"] }]))), + // Rest element in destructuring with ignoreDestructuring + ( + "const { category_id, ...other_props } = obj;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + // Array destructuring with ignoreDestructuring + ( + "const [foo_bar, bar_baz] = arr;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + ("const [foo_bar = 1] = arr;", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), ]; + // Test cases to verify current gaps (should fail but currently pass - known limitations) + // These are documented as ESLint compatibility gaps: + // 1. Rest element without ignoreDestructuring - should report other_props + // 2. Array destructuring - should report foo_bar + // 3. ignoreDestructuring + later use - ESLint reports but we don't (scope analysis needed) + let fail = vec![ (r#"first_name = "Nicholas""#, None), (r#"__private_first_name = "Patrick""#, None), @@ -610,6 +668,15 @@ fn test() { r#"import { snake_case as other_snake } from "mod";"#, Some(serde_json::json!([{ "ignoreImports": true }])), ), + // Rest element in destructuring (without ignoreDestructuring) - should report + ("const { foo, ...other_props } = obj;", None), + // Array destructuring - should report + ("const [foo_bar] = arr;", None), + ("const [first, second_item] = arr;", None), + ("const [foo_bar = 1] = arr;", None), // with default value + // NOTE: Known ESLint compatibility gaps (not currently checked): + // - ignoreDestructuring + later use: ESLint checks variable usage after destructuring + // - ignoreGlobals option: not implemented ]; Tester::new(Camelcase::NAME, Camelcase::PLUGIN, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/eslint_camelcase.snap b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap index 0ab0dc174e726..82f2df407e8e5 100644 --- a/crates/oxc_linter/src/snapshots/eslint_camelcase.snap +++ b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap @@ -259,3 +259,31 @@ source: crates/oxc_linter/src/tester.rs · ─────────── ╰──── help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'other_props' is not in camel case. + ╭─[camelcase.tsx:1:17] + 1 │ const { foo, ...other_props } = obj; + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ const [foo_bar] = arr; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'second_item' is not in camel case. + ╭─[camelcase.tsx:1:15] + 1 │ const [first, second_item] = arr; + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ const [foo_bar = 1] = arr; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. From 28e49975bc639d6d2c21f27aef44310c1d7f01f7 Mon Sep 17 00:00:00 2001 From: Tu Shaokun <2801884530@qq.com> Date: Tue, 16 Dec 2025 10:08:55 +0800 Subject: [PATCH 4/8] fix(linter): array destructuring should not be skipped by ignoreDestructuring ESLint's ignoreDestructuring only skips object destructuring where local name equals property name. Array destructuring has no property names (only indices), so ESLint always checks array elements. - Remove ignoreDestructuring check from check_binding_pattern - Move array destructuring test cases to fail section - Update comments to reflect actual ESLint behavior --- .../oxc_linter/src/rules/eslint/camelcase.rs | 36 +++++++++---------- .../src/snapshots/eslint_camelcase.snap | 21 +++++++++++ 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/camelcase.rs b/crates/oxc_linter/src/rules/eslint/camelcase.rs index 22c82eaa4b7ba..ac7818a1f191c 100644 --- a/crates/oxc_linter/src/rules/eslint/camelcase.rs +++ b/crates/oxc_linter/src/rules/eslint/camelcase.rs @@ -417,14 +417,12 @@ impl Camelcase { } /// Check a binding pattern for camelCase violations (used for array destructuring) + /// Note: ignoreDestructuring does NOT apply to array destructuring because + /// array elements have no "property name" to compare against (only indices). + /// ESLint always checks array destructuring bindings regardless of ignoreDestructuring. fn check_binding_pattern(&self, pattern: &oxc_ast::ast::BindingPattern, ctx: &LintContext) { match &pattern.kind { BindingPatternKind::BindingIdentifier(ident) => { - // For array destructuring, there's no "property name" to compare - // so ignoreDestructuring skips all array element bindings - if self.0.ignore_destructuring { - return; - } self.check_name(&ident.name, ident.span, ctx); } BindingPatternKind::AssignmentPattern(assign) => { @@ -595,24 +593,13 @@ fn test() { ("foo_bar = 0;", Some(serde_json::json!([{ "allow": ["foo.*"] }]))), ("get_user_id = 0;", Some(serde_json::json!([{ "allow": ["_id"] }]))), // Rest element in destructuring with ignoreDestructuring + // Rest elements have no "property name" to compare, so they're skipped ( "const { category_id, ...other_props } = obj;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), ), - // Array destructuring with ignoreDestructuring - ( - "const [foo_bar, bar_baz] = arr;", - Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), - ("const [foo_bar = 1] = arr;", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), ]; - // Test cases to verify current gaps (should fail but currently pass - known limitations) - // These are documented as ESLint compatibility gaps: - // 1. Rest element without ignoreDestructuring - should report other_props - // 2. Array destructuring - should report foo_bar - // 3. ignoreDestructuring + later use - ESLint reports but we don't (scope analysis needed) - let fail = vec![ (r#"first_name = "Nicholas""#, None), (r#"__private_first_name = "Patrick""#, None), @@ -674,9 +661,18 @@ fn test() { ("const [foo_bar] = arr;", None), ("const [first, second_item] = arr;", None), ("const [foo_bar = 1] = arr;", None), // with default value - // NOTE: Known ESLint compatibility gaps (not currently checked): - // - ignoreDestructuring + later use: ESLint checks variable usage after destructuring - // - ignoreGlobals option: not implemented + // Array destructuring is NOT skipped by ignoreDestructuring (no property name to compare) + ( + "const [foo_bar, bar_baz] = arr;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + ( + "const [foo_bar = 1] = arr;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + // NOTE: Known ESLint compatibility gaps (not currently checked): + // - ignoreDestructuring + later use: ESLint checks variable usage after destructuring + // - ignoreGlobals option: not implemented ]; Tester::new(Camelcase::NAME, Camelcase::PLUGIN, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/eslint_camelcase.snap b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap index 82f2df407e8e5..b7986ab67517a 100644 --- a/crates/oxc_linter/src/snapshots/eslint_camelcase.snap +++ b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap @@ -287,3 +287,24 @@ source: crates/oxc_linter/src/tester.rs · ─────── ╰──── help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ const [foo_bar, bar_baz] = arr; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. + ╭─[camelcase.tsx:1:17] + 1 │ const [foo_bar, bar_baz] = arr; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ const [foo_bar = 1] = arr; + · ─────── + ╰──── + help: Rename this identifier to use camelCase. From f717c0b2a261ab61ad8d76d680bbcad75eca986a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:10:03 +0000 Subject: [PATCH 5/8] [autofix.ci] apply automated fixes --- crates/oxc_linter/src/rules/eslint/camelcase.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/camelcase.rs b/crates/oxc_linter/src/rules/eslint/camelcase.rs index ac7818a1f191c..6795d906c83bf 100644 --- a/crates/oxc_linter/src/rules/eslint/camelcase.rs +++ b/crates/oxc_linter/src/rules/eslint/camelcase.rs @@ -666,10 +666,7 @@ fn test() { "const [foo_bar, bar_baz] = arr;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), ), - ( - "const [foo_bar = 1] = arr;", - Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), + ("const [foo_bar = 1] = arr;", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), // NOTE: Known ESLint compatibility gaps (not currently checked): // - ignoreDestructuring + later use: ESLint checks variable usage after destructuring // - ignoreGlobals option: not implemented From efbe9f64c2f21a0862d4ca95a9b2c84a4ac4ab42 Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Tue, 16 Dec 2025 18:01:14 +0000 Subject: [PATCH 6/8] re-port-test-cases --- .../oxc_linter/src/rules/eslint/camelcase.rs | 438 ++++++++++++++---- 1 file changed, 337 insertions(+), 101 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/camelcase.rs b/crates/oxc_linter/src/rules/eslint/camelcase.rs index 6795d906c83bf..ac420afdd6970 100644 --- a/crates/oxc_linter/src/rules/eslint/camelcase.rs +++ b/crates/oxc_linter/src/rules/eslint/camelcase.rs @@ -523,43 +523,100 @@ fn test() { Some(serde_json::json!([{ "properties": "never" }])), ), ("obj.foo_bar = function(){};", Some(serde_json::json!([{ "properties": "never" }]))), - ("const { ['foo']: _foo } = obj;", None), - ("const { [_foo_]: foo } = obj;", None), + ("const { ['foo']: _foo } = obj;", None), // { "ecmaVersion": 6 }, + ("const { [_foo_]: foo } = obj;", None), // { "ecmaVersion": 6 }, ( "var { category_id } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), + ), // { "ecmaVersion": 6 }, ( "var { category_id: category_id } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), + ), // { "ecmaVersion": 6 }, ( "var { category_id = 1 } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), - ("var { category_id: category } = query;", None), - ("var { _leading } = query;", None), - ("var { trailing_ } = query;", None), - (r#"import { camelCased } from "external module";"#, None), - (r#"import { _leading } from "external module";"#, None), - (r#"import { trailing_ } from "external module";"#, None), - (r#"import { no_camelcased as camelCased } from "external-module";"#, None), - (r#"import { no_camelcased as _leading } from "external-module";"#, None), - (r#"import { no_camelcased as trailing_ } from "external-module";"#, None), + ), // { "ecmaVersion": 6 }, + ( + "var { [{category_id} = query]: categoryId } = query;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), // { "ecmaVersion": 6 }, + ("var { category_id: category } = query;", None), // { "ecmaVersion": 6 }, + ("var { _leading } = query;", None), // { "ecmaVersion": 6 }, + ("var { trailing_ } = query;", None), // { "ecmaVersion": 6 }, + (r#"import { camelCased } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import { _leading } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import { trailing_ } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import { no_camelcased as camelCased } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import { no_camelcased as _leading } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import { no_camelcased as trailing_ } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, ( r#"import { no_camelcased as camelCased, anotherCamelCased } from "external-module";"#, None, - ), - ("import { snake_cased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }]))), - ("function foo({ no_camelcased: camelCased }) {};", None), - ("function foo({ no_camelcased: _leading }) {};", None), - ("function foo({ no_camelcased: trailing_ }) {};", None), - ("function foo({ camelCased = 'default value' }) {};", None), - ("function foo({ _leading = 'default value' }) {};", None), - ("function foo({ trailing_ = 'default value' }) {};", None), - ("function foo({ camelCased }) {};", None), - ("function foo({ _leading }) {}", None), - ("function foo({ trailing_ }) {}", None), + ), // { "ecmaVersion": 6, "sourceType": "module" }, + ("import { snake_cased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }]))), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + "import { snake_cased as snake_cased } from 'mod'", + Some(serde_json::json!([{ "ignoreImports": true }])), + ), // { "ecmaVersion": 2022, "sourceType": "module" }, + ( + "import { 'snake_cased' as snake_cased } from 'mod'", + Some(serde_json::json!([{ "ignoreImports": true }])), + ), // { "ecmaVersion": 2022, "sourceType": "module" }, + ("import { camelCased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": false }]))), // { "ecmaVersion": 6, "sourceType": "module" }, + ("export { a as 'snake_cased' } from 'mod'", None), // { "ecmaVersion": 2022, "sourceType": "module" }, + ("export * as 'snake_cased' from 'mod'", None), // { "ecmaVersion": 2022, "sourceType": "module" }, + ( + "var _camelCased = aGlobalVariable", + Some(serde_json::json!([{ "ignoreGlobals": false }])), + ), // { "globals": { "aGlobalVariable": "readonly" } }, + ( + "var camelCased = _aGlobalVariable", + Some(serde_json::json!([{ "ignoreGlobals": false }])), + ), // { "globals": { _"aGlobalVariable": "readonly" } }, + ( + "var camelCased = a_global_variable", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "globals": { "a_global_variable": "readonly" } }, + ("a_global_variable.foo()", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, + ("a_global_variable[undefined]", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, + ("var foo = a_global_variable.bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, + ("a_global_variable.foo = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, + ( + "( { foo: a_global_variable.bar } = baz )", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "readonly", }, }, + ("a_global_variable = foo", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "writable" } }, + ("a_global_variable = foo", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, + ("({ a_global_variable } = foo)", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ( + "({ snake_cased: a_global_variable } = foo)", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ( + "({ snake_cased: a_global_variable = foo } = bar)", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ("[a_global_variable] = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ("[a_global_variable = foo] = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ("foo[a_global_variable] = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly", }, }, + ( + "var foo = { [a_global_variable]: bar }", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "readonly", }, }, + ( + "var { [a_global_variable]: foo } = bar", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "readonly", }, }, + ("function foo({ no_camelcased: camelCased }) {};", None), // { "ecmaVersion": 6 }, + ("function foo({ no_camelcased: _leading }) {};", None), // { "ecmaVersion": 6 }, + ("function foo({ no_camelcased: trailing_ }) {};", None), // { "ecmaVersion": 6 }, + ("function foo({ camelCased = 'default value' }) {};", None), // { "ecmaVersion": 6 }, + ("function foo({ _leading = 'default value' }) {};", None), // { "ecmaVersion": 6 }, + ("function foo({ trailing_ = 'default value' }) {};", None), // { "ecmaVersion": 6 }, + ("function foo({ camelCased }) {};", None), // { "ecmaVersion": 6 }, + ("function foo({ _leading }) {}", None), // { "ecmaVersion": 6 }, + ("function foo({ trailing_ }) {}", None), // { "ecmaVersion": 6 }, ("ignored_foo = 0;", Some(serde_json::json!([{ "allow": ["ignored_foo"] }]))), ( "ignored_foo = 0; ignored_bar = 1;", @@ -567,37 +624,97 @@ fn test() { ), ("user_id = 0;", Some(serde_json::json!([{ "allow": ["_id$"] }]))), ("__option_foo__ = 0;", Some(serde_json::json!([{ "allow": ["__option_foo__"] }]))), + ( + "__option_foo__ = 0; user_id = 0; foo = 1", + Some(serde_json::json!([{ "allow": ["__option_foo__", "_id$"] }])), + ), + ("fo_o = 0;", Some(serde_json::json!([{ "allow": ["__option_foo__", "fo_o"] }]))), + ("user = 0;", Some(serde_json::json!([{ "allow": [] }]))), + ("foo = { [computedBar]: 0 };", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), // { "ecmaVersion": 6 }, + ("({ a: obj.fo_o } = bar);", Some(serde_json::json!([{ "allow": ["fo_o"] }]))), // { "ecmaVersion": 6 }, + ("({ a: obj.foo } = bar);", Some(serde_json::json!([{ "allow": ["fo_o"] }]))), // { "ecmaVersion": 6 }, + ("({ a: obj.fo_o } = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, + ("({ a: obj.fo_o.b_ar } = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, + ("({ a: { b: obj.fo_o } } = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, + ("([obj.fo_o] = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, + ("({ c: [ob.fo_o]} = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, + ("([obj.fo_o.b_ar] = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, + ("({obj} = baz.fo_o);", None), // { "ecmaVersion": 6 }, + ("([obj] = baz.fo_o);", None), // { "ecmaVersion": 6 }, + ("([obj.foo = obj.fo_o] = bar);", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 6 }, ( "class C { camelCase; #camelCase; #camelCase2() {} }", Some(serde_json::json!([{ "properties": "always" }])), - ), + ), // { "ecmaVersion": 2022 }, ( "class C { snake_case; #snake_case; #snake_case2() {} }", Some(serde_json::json!([{ "properties": "never" }])), - ), - // ignoreDestructuring applies to destructuring assignments too (only shorthand/same-name) - ("({ foo_bar } = obj);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), - // ESLint: ignoreDestructuring only skips when local === property name - // { category_id: camelCase } -> camelCase is valid, so pass + ), // { "ecmaVersion": 2022 }, ( - "var { category_id: camelCase } = query;", - Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), - // ignoreImports only skips when local === imported (ESLint behavior) - // import { snake_case as camelCase } -> camelCase is valid, so pass + " + const { some_property } = obj; + + const bar = { some_property }; + + obj.some_property = 10; + + const xyz = { some_property: obj.some_property }; + + const foo = ({ some_property }) => { + console.log(some_property) + }; + ", + Some(serde_json::json!([{ "properties": "never", "ignoreDestructuring": true }])), + ), // { "ecmaVersion": 2022 }, ( - r#"import { snake_case as camelCase } from "mod";"#, - Some(serde_json::json!([{ "ignoreImports": true }])), - ), - // allow patterns work as regex even without ^/$ - ("foo_bar = 0;", Some(serde_json::json!([{ "allow": ["foo.*"] }]))), - ("get_user_id = 0;", Some(serde_json::json!([{ "allow": ["_id"] }]))), - // Rest element in destructuring with ignoreDestructuring - // Rest elements have no "property name" to compare, so they're skipped + " + const { some_property } = obj; + doSomething({ some_property }); + ", + Some(serde_json::json!([{ "properties": "never", "ignoreDestructuring": true }])), + ), // { "ecmaVersion": 2022 }, ( - "const { category_id, ...other_props } = obj;", - Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), + "import foo from 'foo.json' with { my_type: 'json' }", + Some( + serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + ), + ), // { "ecmaVersion": 2025, "sourceType": "module" }, + ( + "export * from 'foo.json' with { my_type: 'json' }", + Some( + serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + ), + ), // { "ecmaVersion": 2025, "sourceType": "module" }, + ( + "export { default } from 'foo.json' with { my_type: 'json' }", + Some( + serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + ), + ), // { "ecmaVersion": 2025, "sourceType": "module" }, + ( + "import('foo.json', { my_with: { my_type: 'json' } })", + Some( + serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + ), + ), // { "ecmaVersion": 2025 }, + ( + "import('foo.json', { 'with': { my_type: 'json' } })", + Some( + serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + ), + ), // { "ecmaVersion": 2025 }, + ( + "import('foo.json', { my_with: { my_type } })", + Some( + serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + ), + ), // { "ecmaVersion": 2025 }, + ( + "import('foo.json', { my_with: { my_type } })", + Some( + serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + ), + ), // { "ecmaVersion": 2025, "globals": { "my_type": true, }, } ]; let fail = vec![ @@ -606,70 +723,189 @@ fn test() { ("function foo_bar(){}", None), ("obj.foo_bar = function(){};", None), ("bar_baz.foo = function(){};", None), + ("[foo_bar.baz]", None), + ("if (foo.bar_baz === boom.bam_pow) { [foo_bar.baz] }", None), + ("foo.bar_baz = boom.bam_pow", None), ("var foo = { bar_baz: boom.bam_pow }", None), + ( + "var foo = { bar_baz: boom.bam_pow }", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), + ("foo.qux.boom_pow = { bar: boom.bam_pow }", None), ("var o = {bar_baz: 1}", Some(serde_json::json!([{ "properties": "always" }]))), ("obj.a_b = 2;", Some(serde_json::json!([{ "properties": "always" }]))), - ("var { category_id } = query;", None), - ("var { category_id: category_id } = query;", None), - ("var { category_id = 1 } = query;", None), - (r#"import no_camelcased from "external-module";"#, None), - (r#"import * as no_camelcased from "external-module";"#, None), - (r#"import { no_camelcased } from "external-module";"#, None), - (r#"import { no_camelcased as no_camel_cased } from "external module";"#, None), - (r#"import { camelCased as no_camel_cased } from "external module";"#, None), - (r#"import { camelCased, no_camelcased } from "external-module";"#, None), - ("export * as snake_cased from 'mod'", None), - ("function foo({ no_camelcased }) {};", None), - ("function foo({ no_camelcased = 'default value' }) {};", None), - ("const { bar: no_camelcased } = foo;", None), - ("function foo({ value_1: my_default }) {}", None), - ("function foo({ isCamelcased: no_camelcased }) {};", None), - ("var { foo: bar_baz = 1 } = quz;", None), - ("const { no_camelcased = false } = bar;", None), - ("not_ignored_foo = 0;", Some(serde_json::json!([{ "allow": ["ignored_bar"] }]))), - ("class C { snake_case; }", Some(serde_json::json!([{ "properties": "always" }]))), + ("var { category_id: category_alias } = query;", None), // { "ecmaVersion": 6 }, ( - "class C { #snake_case; foo() { this.#snake_case; } }", - Some(serde_json::json!([{ "properties": "always" }])), - ), - ("class C { #snake_case() {} }", Some(serde_json::json!([{ "properties": "always" }]))), - // Ensure var foo_bar = {} is NOT mistaken for destructuring - ("var foo_bar = {};", None), - ("var foo_bar = [];", None), - // Destructuring assignments (not declarations) - ("({ foo_bar } = obj);", None), - ("({ key: bar_baz } = obj);", None), - // ESLint ignoreDestructuring: renamed destructuring should still report - // { category_id: other_name } -> other_name should be checked (not equal to key) - ( - "var { category_id: other_name } = query;", + "var { category_id: category_alias } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), + ), // { "ecmaVersion": 6 }, ( - "({ key: other_name } = obj);", + "var { [category_id]: categoryId } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), - // ESLint ignoreImports: renamed imports should still report - // import { snake_case as other_snake } -> other_snake should be checked + ), // { "ecmaVersion": 6 }, + ("var { [category_id]: categoryId } = query;", None), // { "ecmaVersion": 6 }, ( - r#"import { snake_case as other_snake } from "mod";"#, + "var { category_id: categoryId, ...other_props } = query;", + Some(serde_json::json!([{ "ignoreDestructuring": true }])), + ), // { "ecmaVersion": 2018 }, + ("var { category_id } = query;", None), // { "ecmaVersion": 6 }, + ("var { category_id: category_id } = query;", None), // { "ecmaVersion": 6 }, + ("var { category_id = 1 } = query;", None), // { "ecmaVersion": 6 }, + (r#"import no_camelcased from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import * as no_camelcased from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import { no_camelcased } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import { no_camelcased as no_camel_cased } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import { camelCased as no_camel_cased } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + ("import { 'snake_cased' as snake_cased } from 'mod'", None), // { "ecmaVersion": 2022, "sourceType": "module" }, + ( + "import { 'snake_cased' as another_snake_cased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }])), - ), - // Rest element in destructuring (without ignoreDestructuring) - should report - ("const { foo, ...other_props } = obj;", None), - // Array destructuring - should report - ("const [foo_bar] = arr;", None), - ("const [first, second_item] = arr;", None), - ("const [foo_bar = 1] = arr;", None), // with default value - // Array destructuring is NOT skipped by ignoreDestructuring (no property name to compare) - ( - "const [foo_bar, bar_baz] = arr;", + ), // { "ecmaVersion": 2022, "sourceType": "module" }, + (r#"import { camelCased, no_camelcased } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + r#"import { no_camelcased as camelCased, another_no_camelcased } from "external-module";"#, + None, + ), // { "ecmaVersion": 6, "sourceType": "module" }, + (r#"import camelCased, { no_camelcased } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + r#"import no_camelcased, { another_no_camelcased as camelCased } from "external-module";"#, + None, + ), // { "ecmaVersion": 6, "sourceType": "module" }, + ("import snake_cased from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }]))), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + "import * as snake_cased from 'mod'", + Some(serde_json::json!([{ "ignoreImports": true }])), + ), // { "ecmaVersion": 6, "sourceType": "module" }, + ("import snake_cased from 'mod'", Some(serde_json::json!([{ "ignoreImports": false }]))), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + "import * as snake_cased from 'mod'", + Some(serde_json::json!([{ "ignoreImports": false }])), + ), // { "ecmaVersion": 6, "sourceType": "module" }, + ("var camelCased = snake_cased", Some(serde_json::json!([{ "ignoreGlobals": false }]))), // { "globals": { "snake_cased": "readonly" } }, + ("a_global_variable.foo()", Some(serde_json::json!([{ "ignoreGlobals": false }]))), // { "globals": { "snake_cased": "readonly" } }, + ("a_global_variable[undefined]", Some(serde_json::json!([{ "ignoreGlobals": false }]))), // { "globals": { "snake_cased": "readonly" } }, + ("var camelCased = snake_cased", None), // { "globals": { "snake_cased": "readonly" } }, + ("var camelCased = snake_cased", Some(serde_json::json!([{}]))), // { "globals": { "snake_cased": "readonly" } }, + ("foo.a_global_variable = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "writable" } }, + ( + "var foo = { a_global_variable: bar }", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "globals": { "a_global_variable": "writable" } }, + ( + "var foo = { a_global_variable: a_global_variable }", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "globals": { "a_global_variable": "writable" } }, + ( + "var foo = { a_global_variable() {} }", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ( + "class Foo { a_global_variable() {} }", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ("a_global_variable: for (;;);", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "writable", }, }, + ( + "if (foo) { let a_global_variable; a_global_variable = bar; }", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ( + "function foo(a_global_variable) { foo = a_global_variable; }", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ("var a_global_variable", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6 }, + ("function a_global_variable () {}", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6 }, + ( + "const a_global_variable = foo; bar = a_global_variable", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ( + "bar = a_global_variable; var a_global_variable;", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ("var foo = { a_global_variable }", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6, "globals": { "a_global_variable": "readonly", }, }, + ("undefined_variable;", Some(serde_json::json!([{ "ignoreGlobals": true }]))), + ("implicit_global = 1;", Some(serde_json::json!([{ "ignoreGlobals": true }]))), + ("export * as snake_cased from 'mod'", None), // { "ecmaVersion": 2020, "sourceType": "module" }, + ("function foo({ no_camelcased }) {};", None), // { "ecmaVersion": 6 }, + ("function foo({ no_camelcased = 'default value' }) {};", None), // { "ecmaVersion": 6 }, + ("const no_camelcased = 0; function foo({ camelcased_value = no_camelcased}) {}", None), // { "ecmaVersion": 6 }, + ("const { bar: no_camelcased } = foo;", None), // { "ecmaVersion": 6 }, + ("function foo({ value_1: my_default }) {}", None), // { "ecmaVersion": 6 }, + ("function foo({ isCamelcased: no_camelcased }) {};", None), // { "ecmaVersion": 6 }, + ("var { foo: bar_baz = 1 } = quz;", None), // { "ecmaVersion": 6 }, + ("const { no_camelcased = false } = bar;", None), // { "ecmaVersion": 6 }, + ("const { no_camelcased = foo_bar } = bar;", None), // { "ecmaVersion": 6 }, + ("not_ignored_foo = 0;", Some(serde_json::json!([{ "allow": ["ignored_bar"] }]))), + ("not_ignored_foo = 0;", Some(serde_json::json!([{ "allow": ["_id$"] }]))), + ( + "foo = { [computed_bar]: 0 };", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), - ("const [foo_bar = 1] = arr;", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), - // NOTE: Known ESLint compatibility gaps (not currently checked): - // - ignoreDestructuring + later use: ESLint checks variable usage after destructuring - // - ignoreGlobals option: not implemented + ), // { "ecmaVersion": 6 }, + ("({ a: obj.fo_o } = bar);", None), // { "ecmaVersion": 6 }, + ("({ a: obj.fo_o } = bar);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), // { "ecmaVersion": 6 }, + ("({ a: obj.fo_o.b_ar } = baz);", None), // { "ecmaVersion": 6 }, + ("({ a: { b: { c: obj.fo_o } } } = bar);", None), // { "ecmaVersion": 6 }, + ("({ a: { b: { c: obj.fo_o.b_ar } } } = baz);", None), // { "ecmaVersion": 6 }, + ("([obj.fo_o] = bar);", None), // { "ecmaVersion": 6 }, + ("([obj.fo_o] = bar);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), // { "ecmaVersion": 6 }, + ("([obj.fo_o = 1] = bar);", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 6 }, + ("({ a: [obj.fo_o] } = bar);", None), // { "ecmaVersion": 6 }, + ("({ a: { b: [obj.fo_o] } } = bar);", None), // { "ecmaVersion": 6 }, + ("([obj.fo_o.ba_r] = baz);", None), // { "ecmaVersion": 6 }, + ("({...obj.fo_o} = baz);", None), // { "ecmaVersion": 9 }, + ("({...obj.fo_o.ba_r} = baz);", None), // { "ecmaVersion": 9 }, + ("({c: {...obj.fo_o }} = baz);", None), // { "ecmaVersion": 9 }, + ("obj.o_k.non_camelcase = 0", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 2020 }, + ("(obj?.o_k).non_camelcase = 0", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 2020 }, + ("class C { snake_case; }", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 2022 }, + ( + "class C { #snake_case; foo() { this.#snake_case; } }", + Some(serde_json::json!([{ "properties": "always" }])), + ), // { "ecmaVersion": 2022 }, + ("class C { #snake_case() {} }", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 2022 }, + ( + " + const { some_property } = obj; + doSomething({ some_property }); + ", + Some(serde_json::json!([{ "properties": "always", "ignoreDestructuring": true }])), + ), // { "ecmaVersion": 2022 }, + ( + r#" + const { some_property } = obj; + doSomething({ some_property }); + doSomething({ [some_property]: "bar" }); + "#, + Some(serde_json::json!([{ "properties": "never", "ignoreDestructuring": true }])), + ), // { "ecmaVersion": 2022 }, + ( + " + const { some_property } = obj; + + const bar = { some_property }; + + obj.some_property = 10; + + const xyz = { some_property: obj.some_property }; + + const foo = ({ some_property }) => { + console.log(some_property) + }; + ", + Some(serde_json::json!([{ "properties": "always", "ignoreDestructuring": true }])), + ), // { "ecmaVersion": 2022 }, + ( + "import('foo.json', { my_with: { [my_type]: 'json' } })", + Some( + serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + ), + ), // { "ecmaVersion": 2025 }, + ( + "import('foo.json', { my_with: { my_type: my_json } })", + Some( + serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + ), + ), // { "ecmaVersion": 2025 } ]; Tester::new(Camelcase::NAME, Camelcase::PLUGIN, pass, fail).test_and_snapshot(); From 07ef1b6d3209d08e8bf24582c8adb0826e79f340 Mon Sep 17 00:00:00 2001 From: Tu Shaokun <2801884530@qq.com> Date: Wed, 17 Dec 2025 14:59:42 +0800 Subject: [PATCH 7/8] refactor(linter): rewrite eslint/camelcase using semantic analysis - Replace AST-handler approach with `run_once` + semantic symbol/reference iteration - Fix ignoreGlobals to only skip explicitly configured globals + env globals (matching ESLint behavior, not skipping all unresolved references) - Add equalsToOriginalName support for AssignmentTargetPropertyProperty (handles `({ foo: foo } = obj)` pattern) - Fix AssignmentPattern to only skip identifiers on the `right` side - Split tests requiring globals config into separate Tester run - Add fail tests for undefined variables with ignoreGlobals: true --- .../src/generated/rule_runner_impls.rs | 14 +- .../oxc_linter/src/rules/eslint/camelcase.rs | 1132 ++++++++++------- .../src/snapshots/eslint_camelcase.snap | 561 +++++++- 3 files changed, 1192 insertions(+), 515 deletions(-) diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 367dc321216dd..187d7a5c5ee6d 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -39,29 +39,19 @@ impl RuleRunner for crate::rules::eslint::block_scoped_var::BlockScopedVar { impl RuleRunner for crate::rules::eslint::camelcase::Camelcase { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ - AstType::ArrayPattern, AstType::AssignmentExpression, - AstType::AssignmentTargetPropertyIdentifier, - AstType::AssignmentTargetPropertyProperty, - AstType::BindingProperty, - AstType::BindingRestElement, AstType::BreakStatement, AstType::ContinueStatement, AstType::ExportAllDeclaration, AstType::ExportSpecifier, - AstType::FormalParameter, - AstType::Function, - AstType::ImportDefaultSpecifier, - AstType::ImportNamespaceSpecifier, - AstType::ImportSpecifier, AstType::LabeledStatement, AstType::MethodDefinition, AstType::ObjectProperty, AstType::PrivateIdentifier, AstType::PropertyDefinition, - AstType::VariableDeclarator, + AstType::UpdateExpression, ])); - const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown; } impl RuleRunner for crate::rules::eslint::class_methods_use_this::ClassMethodsUseThis { diff --git a/crates/oxc_linter/src/rules/eslint/camelcase.rs b/crates/oxc_linter/src/rules/eslint/camelcase.rs index ac420afdd6970..da5a30260f1a1 100644 --- a/crates/oxc_linter/src/rules/eslint/camelcase.rs +++ b/crates/oxc_linter/src/rules/eslint/camelcase.rs @@ -1,13 +1,22 @@ use lazy_regex::Regex; use oxc_ast::AstKind; -use oxc_ast::ast::{AssignmentTarget, BindingPatternKind, Expression, PropertyKey}; +use oxc_ast::ast::{ + AssignmentTarget, AssignmentTargetMaybeDefault, BindingPatternKind, ImportAttributeKey, + ImportDeclarationSpecifier, PropertyKey, +}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_span::{GetSpan, Span}; +use oxc_syntax::node::NodeId; +use rustc_hash::FxHashSet; use schemars::JsonSchema; use serde::Deserialize; -use crate::{AstNode, context::LintContext, rule::Rule}; +use crate::{ + AstNode, + context::LintContext, + rule::{DefaultRuleConfig, Rule}, +}; fn camelcase_diagnostic(name: &str, span: Span) -> OxcDiagnostic { OxcDiagnostic::warn(format!("Identifier '{name}' is not in camel case.")) @@ -24,8 +33,6 @@ enum PropertiesOption { } /// Pre-compiled allow pattern -/// ESLint treats each entry as both a literal string AND a regex pattern: -/// `allow.some(entry => name === entry || name.match(new RegExp(entry, "u")))` #[derive(Debug, Clone)] struct AllowPattern { literal: String, @@ -34,13 +41,11 @@ struct AllowPattern { impl AllowPattern { fn new(pattern: String) -> Self { - // Try to compile as regex (ESLint uses Unicode flag) let regex = Regex::new(&pattern).ok(); Self { literal: pattern, regex } } fn matches(&self, name: &str) -> bool { - // ESLint: name === entry || name.match(new RegExp(entry, "u")) if name == self.literal { return true; } @@ -56,43 +61,46 @@ impl AllowPattern { #[derive(Debug, Default, Clone, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase")] pub struct CamelcaseConfig { - /// When set to "never", the rule will not check property names. properties: PropertiesOption, - /// When set to true, the rule will not check destructuring identifiers. ignore_destructuring: bool, - /// When set to true, the rule will not check import identifiers. ignore_imports: bool, - /// An array of names or regex patterns to allow. - /// Patterns starting with `^` or ending with `$` are treated as regular expressions. + ignore_globals: bool, #[serde(default)] allow: Vec, } -/// Runtime configuration with pre-compiled patterns -#[derive(Debug, Clone, Default)] +#[derive(Debug, Default, Clone)] struct CamelcaseRuntime { properties: PropertiesOption, ignore_destructuring: bool, ignore_imports: bool, + ignore_globals: bool, allow_patterns: Vec, } impl From for CamelcaseRuntime { fn from(config: CamelcaseConfig) -> Self { let allow_patterns = config.allow.into_iter().map(AllowPattern::new).collect(); - Self { properties: config.properties, ignore_destructuring: config.ignore_destructuring, ignore_imports: config.ignore_imports, + ignore_globals: config.ignore_globals, allow_patterns, } } } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(from = "CamelcaseConfig")] pub struct Camelcase(Box); +impl From for Camelcase { + fn from(config: CamelcaseConfig) -> Self { + Self(Box::new(config.into())) + } +} + declare_oxc_lint!( /// ### What it does /// @@ -101,8 +109,6 @@ declare_oxc_lint!( /// ### Why is this bad? /// /// Inconsistent naming conventions make code harder to read and maintain. - /// The camelCase convention is widely used in JavaScript and helps maintain - /// a consistent codebase. /// /// ### Examples /// @@ -118,8 +124,6 @@ declare_oxc_lint!( /// var myVariable = 1; /// function doSomething() {} /// obj.myProp = 2; - /// var CONSTANT_VALUE = 1; // all caps allowed - /// var _privateVar = 1; // leading underscore allowed /// ``` Camelcase, eslint, @@ -129,112 +133,91 @@ declare_oxc_lint!( impl Rule for Camelcase { fn from_configuration(value: serde_json::Value) -> Self { - let config: CamelcaseConfig = - value.get(0).and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default(); - Self(Box::new(config.into())) + serde_json::from_value::>(value).unwrap_or_default().into_inner() } - fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { - match node.kind() { - // Variable declarations: var foo_bar = 1; - AstKind::VariableDeclarator(decl) => { - // Only check simple binding identifiers, not destructuring patterns - // Destructuring (ObjectPattern/ArrayPattern) is handled by BindingProperty - if let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind { - self.check_name(&ident.name, ident.span, ctx); - } + fn run_once(&self, ctx: &LintContext) { + // Pre-compute positions to skip (equalsToOriginalName) + let skip_positions = self.compute_skip_positions(ctx); + let mut reported: FxHashSet = FxHashSet::default(); + + // 1. Check all declared symbols + for symbol_id in ctx.scoping().symbol_ids() { + let name = ctx.scoping().symbol_name(symbol_id); + let span = ctx.scoping().symbol_span(symbol_id); + + // Check the symbol declaration (skip if equalsToOriginalName) + if !skip_positions.contains(&span.start) { + self.report_if_bad(name, span, &mut reported, ctx); } - // Destructuring patterns in variable declarations - AstKind::BindingProperty(prop) => { - // Get the local binding name - let local_name = match &prop.value.kind { - BindingPatternKind::BindingIdentifier(ident) => Some(&ident.name), - BindingPatternKind::AssignmentPattern(pattern) => { - if let BindingPatternKind::BindingIdentifier(ident) = &pattern.left.kind { - Some(&ident.name) - } else { - None - } - } - _ => None, - }; + // Check references to this symbol + for reference in ctx.scoping().get_resolved_references(symbol_id) { + let ref_node = ctx.nodes().get_node(reference.node_id()); + let ref_span = ref_node.span(); - let Some(local_name) = local_name else { - return; - }; - - // ESLint ignoreDestructuring: only skip when local name equals property name - // e.g., { category_id } or { category_id: category_id } -> skip - // but { category_id: categoryId } -> still check categoryId - if self.0.ignore_destructuring { - let key_name = match &prop.key { - PropertyKey::StaticIdentifier(ident) => Some(ident.name.as_str()), - _ => None, - }; - if key_name == Some(local_name.as_str()) { - return; - } + // Skip if equalsToOriginalName (shorthand property, etc.) + if skip_positions.contains(&ref_span.start) { + continue; } - // Check the local binding name - match &prop.value.kind { - BindingPatternKind::BindingIdentifier(ident) => { - self.check_name(&ident.name, ident.span, ctx); - } - BindingPatternKind::AssignmentPattern(pattern) => { - if let BindingPatternKind::BindingIdentifier(ident) = &pattern.left.kind { - self.check_name(&ident.name, ident.span, ctx); - } - } - _ => {} + // Skip certain reference contexts (call, new, assignment pattern default) + if Self::should_skip_reference(reference.node_id(), ctx) { + continue; } - } - // Rest element in destructuring: { ...other_props } or [...rest] - AstKind::BindingRestElement(rest) => { - // Get the binding name from the rest element's argument - if let BindingPatternKind::BindingIdentifier(ident) = &rest.argument.kind { - // Check if we should skip due to ignoreDestructuring - // Rest elements don't have a "property name" to compare, so they're - // only skipped when ignoreDestructuring is true (ESLint behavior) - if self.0.ignore_destructuring { - return; - } - self.check_name(&ident.name, ident.span, ctx); - } + self.report_if_bad(name, ref_span, &mut reported, ctx); } + } - // Array destructuring: const [foo_bar, bar_baz] = arr; - AstKind::ArrayPattern(pattern) => { - for element in pattern.elements.iter().flatten() { - self.check_binding_pattern(element, ctx); - } + // 2. Check unresolved (through) references + for (name, reference_ids) in ctx.scoping().root_unresolved_references() { + // ignoreGlobals: skip references to variables that are globals + // This includes both explicit globals config and env-provided globals (like browser/node builtins) + if self.0.ignore_globals + && (ctx.globals().is_enabled(*name) || ctx.env_contains_var(name)) + { + continue; } - // Function declarations: function foo_bar() {} - AstKind::Function(func) => { - if let Some(ident) = &func.id { - self.check_name(&ident.name, ident.span, ctx); + for &reference_id in reference_ids { + let reference = ctx.scoping().get_reference(reference_id); + let ref_node = ctx.nodes().get_node(reference.node_id()); + let ref_span = ref_node.span(); + + // Skip if equalsToOriginalName + if skip_positions.contains(&ref_span.start) { + continue; } - } - // Function/method parameters - AstKind::FormalParameter(param) => { - if let BindingPatternKind::BindingIdentifier(ident) = ¶m.pattern.kind { - self.check_name(&ident.name, ident.span, ctx); + // Skip certain reference contexts + if Self::should_skip_reference(reference.node_id(), ctx) { + continue; } + + self.report_if_bad(name, ref_span, &mut reported, ctx); } + } + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { // Object property definitions: { foo_bar: 1 } AstKind::ObjectProperty(prop) => { if self.0.properties == PropertiesOption::Never { return; } - // Check property key if it's an identifier - if let PropertyKey::StaticIdentifier(ident) = &prop.key { - self.check_name(&ident.name, ident.span, ctx); + // Skip import attribute keys: import ... with { my_type: 'json' } + if Self::is_import_attribute_key(node, ctx) { + return; + } + + // Check static property key + if let PropertyKey::StaticIdentifier(ident) = &prop.key + && !self.is_good_name(&ident.name) + { + ctx.diagnostic(camelcase_diagnostic(&ident.name, ident.span)); } } @@ -243,240 +226,463 @@ impl Rule for Camelcase { if self.0.properties == PropertiesOption::Never { return; } - - if let PropertyKey::StaticIdentifier(ident) = &prop.key { - self.check_name(&ident.name, ident.span, ctx); + if let PropertyKey::StaticIdentifier(ident) = &prop.key + && !self.is_good_name(&ident.name) + { + ctx.diagnostic(camelcase_diagnostic(&ident.name, ident.span)); } } - // Private identifiers in classes: #foo_bar + // Private identifiers in classes AstKind::PrivateIdentifier(ident) => { if self.0.properties == PropertiesOption::Never { return; } - self.check_name(&ident.name, ident.span, ctx); + if !self.is_good_name(&ident.name) { + ctx.diagnostic(camelcase_diagnostic(&ident.name, ident.span)); + } } - // Method definitions + // Method definitions in classes and objects AstKind::MethodDefinition(method) => { if self.0.properties == PropertiesOption::Never { return; } - - if let PropertyKey::StaticIdentifier(ident) = &method.key { - self.check_name(&ident.name, ident.span, ctx); + if let PropertyKey::StaticIdentifier(ident) = &method.key + && !self.is_good_name(&ident.name) + { + ctx.diagnostic(camelcase_diagnostic(&ident.name, ident.span)); } } - // Assignment expressions - AstKind::AssignmentExpression(assign) => { - match &assign.left { - // Simple identifier assignment: foo_bar = 1 - AssignmentTarget::AssignmentTargetIdentifier(ident) => { - self.check_name(&ident.name, ident.span, ctx); - } - // Member expression assignment: obj.foo_bar = 1 or bar_baz.foo = 1 - AssignmentTarget::StaticMemberExpression(member) => { - // Check the object identifier (bar_baz in bar_baz.foo) - if let Expression::Identifier(obj_ident) = &member.object { - self.check_name(&obj_ident.name, obj_ident.span, ctx); + // Export specifiers: export { foo_bar } or export { camelCase as bar_baz } + AstKind::ExportSpecifier(specifier) => { + // Check the exported name (what it's being exported as) + // ESLint checks the exported name, not the local name + // Skip string literals: export { a as 'snake_cased' } + match &specifier.exported { + oxc_ast::ast::ModuleExportName::IdentifierName(ident) => { + if !self.is_good_name(&ident.name) { + ctx.diagnostic(camelcase_diagnostic(&ident.name, ident.span)); } - // Check the property if properties option is "always" - if self.0.properties != PropertiesOption::Never { - self.check_name(&member.property.name, member.property.span, ctx); + } + oxc_ast::ast::ModuleExportName::IdentifierReference(ident) => { + if !self.is_good_name(&ident.name) { + ctx.diagnostic(camelcase_diagnostic(&ident.name, ident.span)); } } - _ => {} + oxc_ast::ast::ModuleExportName::StringLiteral(_) => { + // String literal exports are allowed + } } } - // Import declarations - AstKind::ImportSpecifier(specifier) => { - // ESLint ignoreImports: only skip when local name equals imported name - // e.g., import { snake_case } -> skip (local === imported) - // but import { snake_case as local_name } -> still check local_name - if self.0.ignore_imports { - let imported_name = specifier.imported.name(); - if specifier.local.name.as_str() == imported_name.as_str() { - return; + // Export all declarations: export * as foo_bar from 'mod' + AstKind::ExportAllDeclaration(decl) => { + if let Some(exported) = &decl.exported { + // Skip string literals: export * as 'snake_cased' from 'mod' + match exported { + oxc_ast::ast::ModuleExportName::IdentifierName(ident) => { + if !self.is_good_name(&ident.name) { + ctx.diagnostic(camelcase_diagnostic(&ident.name, ident.span)); + } + } + oxc_ast::ast::ModuleExportName::IdentifierReference(ident) => { + if !self.is_good_name(&ident.name) { + ctx.diagnostic(camelcase_diagnostic(&ident.name, ident.span)); + } + } + oxc_ast::ast::ModuleExportName::StringLiteral(_) => { + // String literal exports are allowed + } } } - - // Check the local name (the name used in current scope) - self.check_name(&specifier.local.name, specifier.local.span, ctx); } - // Default imports: import foo_bar from 'mod' - // No "imported" name to compare, so ignoreImports doesn't apply - AstKind::ImportDefaultSpecifier(specifier) => { - self.check_name(&specifier.local.name, specifier.local.span, ctx); + // Labels + AstKind::LabeledStatement(stmt) => { + if !self.is_good_name(&stmt.label.name) { + ctx.diagnostic(camelcase_diagnostic(&stmt.label.name, stmt.label.span)); + } } - - // Namespace imports: import * as foo_bar from 'mod' - // No "imported" name to compare, so ignoreImports doesn't apply - AstKind::ImportNamespaceSpecifier(specifier) => { - self.check_name(&specifier.local.name, specifier.local.span, ctx); + AstKind::BreakStatement(stmt) => { + if let Some(label) = &stmt.label + && !self.is_good_name(&label.name) + { + ctx.diagnostic(camelcase_diagnostic(&label.name, label.span)); + } } - - // Export all: export * as foo_bar from 'mod' - AstKind::ExportAllDeclaration(export) => { - if let Some(exported) = &export.exported - && let Some(name) = exported.identifier_name() + AstKind::ContinueStatement(stmt) => { + if let Some(label) = &stmt.label + && !self.is_good_name(&label.name) { - self.check_name(name.as_str(), exported.span(), ctx); + ctx.diagnostic(camelcase_diagnostic(&label.name, label.span)); } } - // Named exports: export { foo_bar } or export { foo as bar_baz } - AstKind::ExportSpecifier(specifier) => { - // Check the exported name (the name visible to importers) - if let Some(name) = specifier.exported.identifier_name() { - self.check_name(name.as_str(), specifier.exported.span(), ctx); + // Assignment expressions - check member expression property on LHS + AstKind::AssignmentExpression(expr) => { + if self.0.properties == PropertiesOption::Never { + return; } + self.check_assignment_target(&expr.left, ctx); } - // Destructuring assignment (not declaration): ({ foo_bar } = obj) - // For shorthand: { foo_bar } = obj - this is always local === key - AstKind::AssignmentTargetPropertyIdentifier(ident) => { - // Shorthand destructuring: local name always equals property name - // ESLint ignoreDestructuring skips this case - if self.0.ignore_destructuring { + // Update expressions: obj.foo_bar++ + AstKind::UpdateExpression(expr) => { + if self.0.properties == PropertiesOption::Never { return; } - self.check_name(&ident.binding.name, ident.binding.span, ctx); + if let oxc_ast::ast::SimpleAssignmentTarget::StaticMemberExpression(member) = + &expr.argument + && !self.is_good_name(&member.property.name) + { + ctx.diagnostic(camelcase_diagnostic( + &member.property.name, + member.property.span, + )); + } } - // For renamed: { key: foo_bar } = obj - check foo_bar - AstKind::AssignmentTargetPropertyProperty(prop) => { - // Get the local binding name - let local_name = - if let oxc_ast::ast::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier( - ident, - ) = &prop.binding + _ => {} + } + } +} + +impl Camelcase { + /// Compute positions (span.start) where identifiers should be skipped + /// due to equalsToOriginalName semantics + fn compute_skip_positions(&self, ctx: &LintContext) -> FxHashSet { + let mut skip_positions = FxHashSet::default(); + + for node in ctx.nodes().iter() { + match node.kind() { + // Destructuring: { foo_bar } or { foo_bar: foo_bar } + AstKind::BindingProperty(prop) => { + if self.0.ignore_destructuring { + let key_name = match &prop.key { + PropertyKey::StaticIdentifier(ident) => Some(ident.name.as_str()), + _ => None, + }; + let value_name = match &prop.value.kind { + BindingPatternKind::BindingIdentifier(ident) => { + Some((ident.name.as_str(), ident.span.start)) + } + BindingPatternKind::AssignmentPattern(pattern) => { + if let BindingPatternKind::BindingIdentifier(ident) = + &pattern.left.kind + { + Some((ident.name.as_str(), ident.span.start)) + } else { + None + } + } + _ => None, + }; + if let (Some(key), Some((value, pos))) = (key_name, value_name) + && key == value + { + skip_positions.insert(pos); + } + } + } + + // Object shorthand property: const o = { some_property } + AstKind::ObjectProperty(prop) => { + if self.0.ignore_destructuring + && prop.shorthand + && let PropertyKey::StaticIdentifier(ident) = &prop.key { - Some(ident.name.as_str()) - } else { - None - }; + // The identifier reference in shorthand should be skipped + // (the property key is the same as the value reference) + skip_positions.insert(ident.span.start); + } + } - let Some(local_name) = local_name else { - return; - }; - - // ESLint ignoreDestructuring: only skip when local name equals property name - if self.0.ignore_destructuring { - let key_name = match &prop.name { - PropertyKey::StaticIdentifier(ident) => Some(ident.name.as_str()), - _ => None, - }; - if key_name == Some(local_name) { - return; + // Import: import { foo_bar } from 'mod' + AstKind::ImportDeclaration(decl) => { + if self.0.ignore_imports + && let Some(specifiers) = &decl.specifiers + { + for spec in specifiers { + if let ImportDeclarationSpecifier::ImportSpecifier(import_spec) = spec { + let imported_name = import_spec.imported.name(); + if imported_name == import_spec.local.name.as_str() { + skip_positions.insert(import_spec.local.span.start); + } + } + } } } - // Check the binding target - if let oxc_ast::ast::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier( - ident, - ) = &prop.binding - { - self.check_name(&ident.name, ident.span, ctx); + // Import attributes: import ... with { my_type: 'json' } + // These keys should always be skipped + AstKind::ImportAttribute(attr) => match &attr.key { + ImportAttributeKey::Identifier(ident) => { + skip_positions.insert(ident.span.start); + } + ImportAttributeKey::StringLiteral(_) => {} + }, + + // Assignment target shorthand: ({ foo_bar } = obj) + AstKind::AssignmentTargetPropertyIdentifier(prop) => { + if self.0.ignore_destructuring { + // Shorthand assignment target - skip the binding + skip_positions.insert(prop.binding.span.start); + } } - } - // Labels - both definition and references - AstKind::LabeledStatement(stmt) => { - self.check_name(&stmt.label.name, stmt.label.span, ctx); + // Assignment target non-shorthand: ({ foo: foo } = obj) + AstKind::AssignmentTargetPropertyProperty(prop) => { + if self.0.ignore_destructuring { + let key_name = match &prop.name { + PropertyKey::StaticIdentifier(ident) => Some(ident.name.as_str()), + _ => None, + }; + let binding_info = Self::get_assignment_target_name(&prop.binding); + if let (Some(key), Some((binding_name, pos))) = (key_name, binding_info) + && key == binding_name + { + skip_positions.insert(pos); + } + } + } + + _ => {} } + } - // break label_name; - AstKind::BreakStatement(stmt) => { - if let Some(label) = &stmt.label { - self.check_name(&label.name, label.span, ctx); + skip_positions + } + + /// Extract the identifier name and span from an AssignmentTargetMaybeDefault + fn get_assignment_target_name<'a>( + target: &'a AssignmentTargetMaybeDefault<'a>, + ) -> Option<(&'a str, u32)> { + match target { + AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(ident) => { + Some((ident.name.as_str(), ident.span.start)) + } + AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(with_default) => { + if let AssignmentTarget::AssignmentTargetIdentifier(ident) = &with_default.binding { + Some((ident.name.as_str(), ident.span.start)) + } else { + None } } + _ => None, + } + } + + /// Check if a reference should be skipped based on its context + /// ESLint skips identifiers in Call/New expressions entirely (backward compatibility) + fn should_skip_reference(node_id: NodeId, ctx: &LintContext) -> bool { + let node = ctx.nodes().get_node(node_id); + let parent = ctx.nodes().parent_node(node_id); + + match parent.kind() { + // Skip ALL identifiers in call expressions (callee, arguments, etc.) + // This is ESLint's backward-compat behavior + AstKind::CallExpression(_) | AstKind::NewExpression(_) => true, + // Skip assignment pattern right side only: function foo(a = foo_bar) {} + // ESLint skips only when the identifier is the `right` of AssignmentPattern + AstKind::AssignmentPattern(pattern) => pattern.right.span() == node.span(), + // Skip shorthand properties in import options + AstKind::ObjectProperty(prop) if prop.shorthand => { + Self::is_inside_import_options(node_id, ctx) + } + _ => false, + } + } - // continue label_name; - AstKind::ContinueStatement(stmt) => { - if let Some(label) = &stmt.label { - self.check_name(&label.name, label.span, ctx); + /// Check if a node is inside import options (second argument to import()) + fn is_inside_import_options(node_id: NodeId, ctx: &LintContext) -> bool { + let mut in_object_chain = false; + + for ancestor in ctx.nodes().ancestors(node_id) { + match ancestor.kind() { + AstKind::ObjectExpression(_) | AstKind::ObjectProperty(_) => { + in_object_chain = true; + } + AstKind::ImportExpression(_) => { + return in_object_chain; } + AstKind::ImportDeclaration(_) + | AstKind::ExportNamedDeclaration(_) + | AstKind::ExportAllDeclaration(_) + | AstKind::ExpressionStatement(_) + | AstKind::Program(_) + | AstKind::Function(_) + | AstKind::ArrowFunctionExpression(_) => return false, + _ => {} } + } + false + } - _ => {} + /// Check if this node is an import attribute key or dynamic import options key + fn is_import_attribute_key(node: &AstNode, ctx: &LintContext) -> bool { + let mut in_object_chain = false; + + // Note: ancestors() already starts from the parent node, not the node itself + for ancestor in ctx.nodes().ancestors(node.id()) { + match ancestor.kind() { + // Static import attributes: import ... with { my_type: 'json' } + AstKind::ImportAttribute(_) | AstKind::WithClause(_) => return true, + + // Track that we're inside an object literal chain + AstKind::ObjectExpression(_) | AstKind::ObjectProperty(_) => { + in_object_chain = true; + } + + // Dynamic import: import('foo.json', { my_with: { my_type: 'json' } }) + // If we're in an object chain and hit ImportExpression, we're in options + AstKind::ImportExpression(_) => return in_object_chain, + + AstKind::ImportDeclaration(_) + | AstKind::ExportNamedDeclaration(_) + | AstKind::ExportAllDeclaration(_) + | AstKind::ExpressionStatement(_) + | AstKind::Program(_) + | AstKind::Function(_) + | AstKind::ArrowFunctionExpression(_) => return false, + + _ => {} + } } + false } -} -impl Camelcase { - /// Check if a name violates the camelCase rule - fn check_name(&self, name: &str, span: Span, ctx: &LintContext) { - if self.is_good_name(name) { - return; + /// Check assignment target for member expression properties + fn check_assignment_target(&self, target: &AssignmentTarget, ctx: &LintContext) { + match target { + AssignmentTarget::StaticMemberExpression(member) => { + if !self.is_good_name(&member.property.name) { + ctx.diagnostic(camelcase_diagnostic( + &member.property.name, + member.property.span, + )); + } + } + AssignmentTarget::ArrayAssignmentTarget(arr) => { + for element in arr.elements.iter().flatten() { + self.check_assignment_target_maybe_default(element, ctx); + } + // Check rest: [...obj.fo_o] = bar + if let Some(rest) = &arr.rest { + self.check_assignment_target(&rest.target, ctx); + } + } + AssignmentTarget::ObjectAssignmentTarget(obj) => { + for prop in &obj.properties { + if let oxc_ast::ast::AssignmentTargetProperty::AssignmentTargetPropertyProperty( + p, + ) = prop + { + self.check_assignment_target_maybe_default(&p.binding, ctx); + } + } + // Check rest: {...obj.fo_o} = bar + if let Some(rest) = &obj.rest { + self.check_assignment_target(&rest.target, ctx); + } + } + _ => {} } - ctx.diagnostic(camelcase_diagnostic(name, span)); } - /// Check a binding pattern for camelCase violations (used for array destructuring) - /// Note: ignoreDestructuring does NOT apply to array destructuring because - /// array elements have no "property name" to compare against (only indices). - /// ESLint always checks array destructuring bindings regardless of ignoreDestructuring. - fn check_binding_pattern(&self, pattern: &oxc_ast::ast::BindingPattern, ctx: &LintContext) { - match &pattern.kind { - BindingPatternKind::BindingIdentifier(ident) => { - self.check_name(&ident.name, ident.span, ctx); + /// Check assignment target maybe default for member expression properties + fn check_assignment_target_maybe_default( + &self, + target: &AssignmentTargetMaybeDefault, + ctx: &LintContext, + ) { + match target { + AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(with_default) => { + self.check_assignment_target(&with_default.binding, ctx); } - BindingPatternKind::AssignmentPattern(assign) => { - // Handle default values: const [foo_bar = 1] = arr; - self.check_binding_pattern(&assign.left, ctx); + AssignmentTargetMaybeDefault::StaticMemberExpression(member) => { + if !self.is_good_name(&member.property.name) { + ctx.diagnostic(camelcase_diagnostic( + &member.property.name, + member.property.span, + )); + } + } + AssignmentTargetMaybeDefault::ArrayAssignmentTarget(arr) => { + for element in arr.elements.iter().flatten() { + self.check_assignment_target_maybe_default(element, ctx); + } + if let Some(rest) = &arr.rest { + self.check_assignment_target(&rest.target, ctx); + } + } + AssignmentTargetMaybeDefault::ObjectAssignmentTarget(obj) => { + for prop in &obj.properties { + if let oxc_ast::ast::AssignmentTargetProperty::AssignmentTargetPropertyProperty( + p, + ) = prop + { + self.check_assignment_target_maybe_default(&p.binding, ctx); + } + } + if let Some(rest) = &obj.rest { + self.check_assignment_target(&rest.target, ctx); + } } - // ObjectPattern and ArrayPattern are handled by their own AstKind handlers + // Other variants don't have properties we care about _ => {} } } - /// Check if a name is acceptable (either camelCase or in the allow list) + /// Report if name is bad, using deduplication + fn report_if_bad( + &self, + name: &str, + span: Span, + reported: &mut FxHashSet, + ctx: &LintContext, + ) { + if reported.contains(&span) { + return; + } + if !self.is_good_name(name) { + reported.insert(span); + ctx.diagnostic(camelcase_diagnostic(name, span)); + } + } + + /// Check if a name is acceptable fn is_good_name(&self, name: &str) -> bool { - // Check pre-compiled allow patterns first if self.0.allow_patterns.iter().any(|p| p.matches(name)) { return true; } - !is_underscored(name) } } -/// Check if a name contains underscores in the middle (not camelCase). -/// Leading and trailing underscores are allowed. -/// ALL_CAPS names (constants) are allowed. +/// Check if a name contains underscores in the middle (not camelCase) fn is_underscored(name: &str) -> bool { - // Strip leading underscores let name = name.trim_start_matches('_'); - // Strip trailing underscores let name = name.trim_end_matches('_'); - // Empty string or single char after stripping is fine if name.is_empty() { return false; } - // Check if it's ALL_CAPS (constant style) - these are allowed if is_all_caps(name) { return false; } - // Check for underscore in the middle name.contains('_') } -/// Check if a name is in ALL_CAPS style (allowed for constants) +/// Check if a name is in ALL_CAPS style fn is_all_caps(name: &str) -> bool { - // Must contain at least one letter let has_letter = name.chars().any(char::is_alphabetic); if !has_letter { return false; } - - // All letters must be uppercase, and underscores/digits are allowed name.chars().all(|c| c.is_uppercase() || c.is_ascii_digit() || c == '_') } @@ -523,100 +729,67 @@ fn test() { Some(serde_json::json!([{ "properties": "never" }])), ), ("obj.foo_bar = function(){};", Some(serde_json::json!([{ "properties": "never" }]))), - ("const { ['foo']: _foo } = obj;", None), // { "ecmaVersion": 6 }, - ("const { [_foo_]: foo } = obj;", None), // { "ecmaVersion": 6 }, + ("const { ['foo']: _foo } = obj;", None), + ("const { [_foo_]: foo } = obj;", None), ( "var { category_id } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 6 }, + ), ( "var { category_id: category_id } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 6 }, + ), ( "var { category_id = 1 } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 6 }, + ), ( "var { [{category_id} = query]: categoryId } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 6 }, - ("var { category_id: category } = query;", None), // { "ecmaVersion": 6 }, - ("var { _leading } = query;", None), // { "ecmaVersion": 6 }, - ("var { trailing_ } = query;", None), // { "ecmaVersion": 6 }, - (r#"import { camelCased } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import { _leading } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import { trailing_ } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import { no_camelcased as camelCased } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import { no_camelcased as _leading } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import { no_camelcased as trailing_ } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + ), + ("var { category_id: category } = query;", None), + ("var { _leading } = query;", None), + ("var { trailing_ } = query;", None), + (r#"import { camelCased } from "external module";"#, None), + (r#"import { _leading } from "external module";"#, None), + (r#"import { trailing_ } from "external module";"#, None), + (r#"import { no_camelcased as camelCased } from "external-module";"#, None), + (r#"import { no_camelcased as _leading } from "external-module";"#, None), + (r#"import { no_camelcased as trailing_ } from "external-module";"#, None), ( r#"import { no_camelcased as camelCased, anotherCamelCased } from "external-module";"#, None, - ), // { "ecmaVersion": 6, "sourceType": "module" }, - ("import { snake_cased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }]))), // { "ecmaVersion": 6, "sourceType": "module" }, + ), + ("import { snake_cased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }]))), ( "import { snake_cased as snake_cased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }])), - ), // { "ecmaVersion": 2022, "sourceType": "module" }, + ), ( "import { 'snake_cased' as snake_cased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }])), - ), // { "ecmaVersion": 2022, "sourceType": "module" }, - ("import { camelCased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": false }]))), // { "ecmaVersion": 6, "sourceType": "module" }, - ("export { a as 'snake_cased' } from 'mod'", None), // { "ecmaVersion": 2022, "sourceType": "module" }, - ("export * as 'snake_cased' from 'mod'", None), // { "ecmaVersion": 2022, "sourceType": "module" }, + ), + ("import { camelCased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": false }]))), + ("export { a as 'snake_cased' } from 'mod'", None), + ("export * as 'snake_cased' from 'mod'", None), ( "var _camelCased = aGlobalVariable", Some(serde_json::json!([{ "ignoreGlobals": false }])), - ), // { "globals": { "aGlobalVariable": "readonly" } }, + ), ( "var camelCased = _aGlobalVariable", Some(serde_json::json!([{ "ignoreGlobals": false }])), - ), // { "globals": { _"aGlobalVariable": "readonly" } }, - ( - "var camelCased = a_global_variable", - Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "globals": { "a_global_variable": "readonly" } }, - ("a_global_variable.foo()", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, - ("a_global_variable[undefined]", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, - ("var foo = a_global_variable.bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, - ("a_global_variable.foo = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, - ( - "( { foo: a_global_variable.bar } = baz )", - Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "readonly", }, }, - ("a_global_variable = foo", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "writable" } }, - ("a_global_variable = foo", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly" } }, - ("({ a_global_variable } = foo)", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, - ( - "({ snake_cased: a_global_variable } = foo)", - Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, - ( - "({ snake_cased: a_global_variable = foo } = bar)", - Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, - ("[a_global_variable] = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, - ("[a_global_variable = foo] = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, - ("foo[a_global_variable] = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "readonly", }, }, - ( - "var foo = { [a_global_variable]: bar }", - Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "readonly", }, }, - ( - "var { [a_global_variable]: foo } = bar", - Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "readonly", }, }, - ("function foo({ no_camelcased: camelCased }) {};", None), // { "ecmaVersion": 6 }, - ("function foo({ no_camelcased: _leading }) {};", None), // { "ecmaVersion": 6 }, - ("function foo({ no_camelcased: trailing_ }) {};", None), // { "ecmaVersion": 6 }, - ("function foo({ camelCased = 'default value' }) {};", None), // { "ecmaVersion": 6 }, - ("function foo({ _leading = 'default value' }) {};", None), // { "ecmaVersion": 6 }, - ("function foo({ trailing_ = 'default value' }) {};", None), // { "ecmaVersion": 6 }, - ("function foo({ camelCased }) {};", None), // { "ecmaVersion": 6 }, - ("function foo({ _leading }) {}", None), // { "ecmaVersion": 6 }, - ("function foo({ trailing_ }) {}", None), // { "ecmaVersion": 6 }, + ), + // Note: ignoreGlobals tests with explicit globals config are in a separate Tester run below + ("function foo({ no_camelcased: camelCased }) {};", None), + ("function foo({ no_camelcased: _leading }) {};", None), + ("function foo({ no_camelcased: trailing_ }) {};", None), + ("function foo({ camelCased = 'default value' }) {};", None), + ("function foo({ _leading = 'default value' }) {};", None), + ("function foo({ trailing_ = 'default value' }) {};", None), + ("function foo({ camelCased }) {};", None), + ("function foo({ _leading }) {}", None), + ("function foo({ trailing_ }) {}", None), ("ignored_foo = 0;", Some(serde_json::json!([{ "allow": ["ignored_foo"] }]))), ( "ignored_foo = 0; ignored_bar = 1;", @@ -630,26 +803,26 @@ fn test() { ), ("fo_o = 0;", Some(serde_json::json!([{ "allow": ["__option_foo__", "fo_o"] }]))), ("user = 0;", Some(serde_json::json!([{ "allow": [] }]))), - ("foo = { [computedBar]: 0 };", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), // { "ecmaVersion": 6 }, - ("({ a: obj.fo_o } = bar);", Some(serde_json::json!([{ "allow": ["fo_o"] }]))), // { "ecmaVersion": 6 }, - ("({ a: obj.foo } = bar);", Some(serde_json::json!([{ "allow": ["fo_o"] }]))), // { "ecmaVersion": 6 }, - ("({ a: obj.fo_o } = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, - ("({ a: obj.fo_o.b_ar } = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, - ("({ a: { b: obj.fo_o } } = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, - ("([obj.fo_o] = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, - ("({ c: [ob.fo_o]} = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, - ("([obj.fo_o.b_ar] = bar);", Some(serde_json::json!([{ "properties": "never" }]))), // { "ecmaVersion": 6 }, - ("({obj} = baz.fo_o);", None), // { "ecmaVersion": 6 }, - ("([obj] = baz.fo_o);", None), // { "ecmaVersion": 6 }, - ("([obj.foo = obj.fo_o] = bar);", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 6 }, + ("foo = { [computedBar]: 0 };", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), + ("({ a: obj.fo_o } = bar);", Some(serde_json::json!([{ "allow": ["fo_o"] }]))), + ("({ a: obj.foo } = bar);", Some(serde_json::json!([{ "allow": ["fo_o"] }]))), + ("({ a: obj.fo_o } = bar);", Some(serde_json::json!([{ "properties": "never" }]))), + ("({ a: obj.fo_o.b_ar } = bar);", Some(serde_json::json!([{ "properties": "never" }]))), + ("({ a: { b: obj.fo_o } } = bar);", Some(serde_json::json!([{ "properties": "never" }]))), + ("([obj.fo_o] = bar);", Some(serde_json::json!([{ "properties": "never" }]))), + ("({ c: [ob.fo_o]} = bar);", Some(serde_json::json!([{ "properties": "never" }]))), + ("([obj.fo_o.b_ar] = bar);", Some(serde_json::json!([{ "properties": "never" }]))), + ("({obj} = baz.fo_o);", None), + ("([obj] = baz.fo_o);", None), + ("([obj.foo = obj.fo_o] = bar);", Some(serde_json::json!([{ "properties": "always" }]))), ( "class C { camelCase; #camelCase; #camelCase2() {} }", Some(serde_json::json!([{ "properties": "always" }])), - ), // { "ecmaVersion": 2022 }, + ), ( "class C { snake_case; #snake_case; #snake_case2() {} }", Some(serde_json::json!([{ "properties": "never" }])), - ), // { "ecmaVersion": 2022 }, + ), ( " const { some_property } = obj; @@ -665,56 +838,42 @@ fn test() { }; ", Some(serde_json::json!([{ "properties": "never", "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 2022 }, + ), ( " const { some_property } = obj; doSomething({ some_property }); ", Some(serde_json::json!([{ "properties": "never", "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 2022 }, + ), ( "import foo from 'foo.json' with { my_type: 'json' }", - Some( - serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), - ), - ), // { "ecmaVersion": 2025, "sourceType": "module" }, + Some(serde_json::json!([{ "properties": "always", "ignoreImports": false }])), + ), ( "export * from 'foo.json' with { my_type: 'json' }", - Some( - serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), - ), - ), // { "ecmaVersion": 2025, "sourceType": "module" }, + Some(serde_json::json!([{ "properties": "always", "ignoreImports": false }])), + ), ( "export { default } from 'foo.json' with { my_type: 'json' }", - Some( - serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), - ), - ), // { "ecmaVersion": 2025, "sourceType": "module" }, + Some(serde_json::json!([{ "properties": "always", "ignoreImports": false }])), + ), ( "import('foo.json', { my_with: { my_type: 'json' } })", - Some( - serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), - ), - ), // { "ecmaVersion": 2025 }, + Some(serde_json::json!([{ "properties": "always", "ignoreImports": false }])), + ), ( "import('foo.json', { 'with': { my_type: 'json' } })", - Some( - serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), - ), - ), // { "ecmaVersion": 2025 }, + Some(serde_json::json!([{ "properties": "always", "ignoreImports": false }])), + ), ( "import('foo.json', { my_with: { my_type } })", - Some( - serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), - ), - ), // { "ecmaVersion": 2025 }, + Some(serde_json::json!([{ "properties": "always", "ignoreImports": false }])), + ), ( "import('foo.json', { my_with: { my_type } })", - Some( - serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), - ), - ), // { "ecmaVersion": 2025, "globals": { "my_type": true, }, } + Some(serde_json::json!([{ "properties": "always", "ignoreImports": false }])), + ), ]; let fail = vec![ @@ -734,142 +893,143 @@ fn test() { ("foo.qux.boom_pow = { bar: boom.bam_pow }", None), ("var o = {bar_baz: 1}", Some(serde_json::json!([{ "properties": "always" }]))), ("obj.a_b = 2;", Some(serde_json::json!([{ "properties": "always" }]))), - ("var { category_id: category_alias } = query;", None), // { "ecmaVersion": 6 }, + ("var { category_id: category_alias } = query;", None), ( "var { category_id: category_alias } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 6 }, + ), ( "var { [category_id]: categoryId } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 6 }, - ("var { [category_id]: categoryId } = query;", None), // { "ecmaVersion": 6 }, + ), + ("var { [category_id]: categoryId } = query;", None), ( "var { category_id: categoryId, ...other_props } = query;", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 2018 }, - ("var { category_id } = query;", None), // { "ecmaVersion": 6 }, - ("var { category_id: category_id } = query;", None), // { "ecmaVersion": 6 }, - ("var { category_id = 1 } = query;", None), // { "ecmaVersion": 6 }, - (r#"import no_camelcased from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import * as no_camelcased from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import { no_camelcased } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import { no_camelcased as no_camel_cased } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import { camelCased as no_camel_cased } from "external module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - ("import { 'snake_cased' as snake_cased } from 'mod'", None), // { "ecmaVersion": 2022, "sourceType": "module" }, + ), + ("var { category_id } = query;", None), + ("var { category_id: category_id } = query;", None), + ("var { category_id = 1 } = query;", None), + (r#"import no_camelcased from "external-module";"#, None), + (r#"import * as no_camelcased from "external-module";"#, None), + (r#"import { no_camelcased } from "external-module";"#, None), + (r#"import { no_camelcased as no_camel_cased } from "external module";"#, None), + (r#"import { camelCased as no_camel_cased } from "external module";"#, None), + ("import { 'snake_cased' as snake_cased } from 'mod'", None), ( "import { 'snake_cased' as another_snake_cased } from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }])), - ), // { "ecmaVersion": 2022, "sourceType": "module" }, - (r#"import { camelCased, no_camelcased } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + ), + (r#"import { camelCased, no_camelcased } from "external-module";"#, None), ( r#"import { no_camelcased as camelCased, another_no_camelcased } from "external-module";"#, None, - ), // { "ecmaVersion": 6, "sourceType": "module" }, - (r#"import camelCased, { no_camelcased } from "external-module";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + ), + (r#"import camelCased, { no_camelcased } from "external-module";"#, None), ( r#"import no_camelcased, { another_no_camelcased as camelCased } from "external-module";"#, None, - ), // { "ecmaVersion": 6, "sourceType": "module" }, - ("import snake_cased from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }]))), // { "ecmaVersion": 6, "sourceType": "module" }, + ), + ("import snake_cased from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }]))), ( "import * as snake_cased from 'mod'", Some(serde_json::json!([{ "ignoreImports": true }])), - ), // { "ecmaVersion": 6, "sourceType": "module" }, - ("import snake_cased from 'mod'", Some(serde_json::json!([{ "ignoreImports": false }]))), // { "ecmaVersion": 6, "sourceType": "module" }, + ), + ("import snake_cased from 'mod'", Some(serde_json::json!([{ "ignoreImports": false }]))), ( "import * as snake_cased from 'mod'", Some(serde_json::json!([{ "ignoreImports": false }])), - ), // { "ecmaVersion": 6, "sourceType": "module" }, - ("var camelCased = snake_cased", Some(serde_json::json!([{ "ignoreGlobals": false }]))), // { "globals": { "snake_cased": "readonly" } }, - ("a_global_variable.foo()", Some(serde_json::json!([{ "ignoreGlobals": false }]))), // { "globals": { "snake_cased": "readonly" } }, - ("a_global_variable[undefined]", Some(serde_json::json!([{ "ignoreGlobals": false }]))), // { "globals": { "snake_cased": "readonly" } }, - ("var camelCased = snake_cased", None), // { "globals": { "snake_cased": "readonly" } }, - ("var camelCased = snake_cased", Some(serde_json::json!([{}]))), // { "globals": { "snake_cased": "readonly" } }, - ("foo.a_global_variable = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "writable" } }, + ), + ("var camelCased = snake_cased", Some(serde_json::json!([{ "ignoreGlobals": false }]))), + ("a_global_variable.foo()", Some(serde_json::json!([{ "ignoreGlobals": false }]))), + ("a_global_variable[undefined]", Some(serde_json::json!([{ "ignoreGlobals": false }]))), + ("var camelCased = snake_cased", None), + ("var camelCased = snake_cased", Some(serde_json::json!([{}]))), + ("foo.a_global_variable = bar", Some(serde_json::json!([{ "ignoreGlobals": true }]))), ( "var foo = { a_global_variable: bar }", Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "globals": { "a_global_variable": "writable" } }, + ), ( "var foo = { a_global_variable: a_global_variable }", Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "globals": { "a_global_variable": "writable" } }, + ), ( "var foo = { a_global_variable() {} }", Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ), ( "class Foo { a_global_variable() {} }", Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, - ("a_global_variable: for (;;);", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "globals": { "a_global_variable": "writable", }, }, + ), + ("a_global_variable: for (;;);", Some(serde_json::json!([{ "ignoreGlobals": true }]))), ( "if (foo) { let a_global_variable; a_global_variable = bar; }", Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ), ( "function foo(a_global_variable) { foo = a_global_variable; }", Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, - ("var a_global_variable", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6 }, - ("function a_global_variable () {}", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6 }, + ), + ("var a_global_variable", Some(serde_json::json!([{ "ignoreGlobals": true }]))), + ("function a_global_variable () {}", Some(serde_json::json!([{ "ignoreGlobals": true }]))), ( "const a_global_variable = foo; bar = a_global_variable", Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, + ), ( "bar = a_global_variable; var a_global_variable;", Some(serde_json::json!([{ "ignoreGlobals": true }])), - ), // { "ecmaVersion": 6, "globals": { "a_global_variable": "writable", }, }, - ("var foo = { a_global_variable }", Some(serde_json::json!([{ "ignoreGlobals": true }]))), // { "ecmaVersion": 6, "globals": { "a_global_variable": "readonly", }, }, - ("undefined_variable;", Some(serde_json::json!([{ "ignoreGlobals": true }]))), - ("implicit_global = 1;", Some(serde_json::json!([{ "ignoreGlobals": true }]))), - ("export * as snake_cased from 'mod'", None), // { "ecmaVersion": 2020, "sourceType": "module" }, - ("function foo({ no_camelcased }) {};", None), // { "ecmaVersion": 6 }, - ("function foo({ no_camelcased = 'default value' }) {};", None), // { "ecmaVersion": 6 }, - ("const no_camelcased = 0; function foo({ camelcased_value = no_camelcased}) {}", None), // { "ecmaVersion": 6 }, - ("const { bar: no_camelcased } = foo;", None), // { "ecmaVersion": 6 }, - ("function foo({ value_1: my_default }) {}", None), // { "ecmaVersion": 6 }, - ("function foo({ isCamelcased: no_camelcased }) {};", None), // { "ecmaVersion": 6 }, - ("var { foo: bar_baz = 1 } = quz;", None), // { "ecmaVersion": 6 }, - ("const { no_camelcased = false } = bar;", None), // { "ecmaVersion": 6 }, - ("const { no_camelcased = foo_bar } = bar;", None), // { "ecmaVersion": 6 }, + ), + ("var foo = { a_global_variable }", Some(serde_json::json!([{ "ignoreGlobals": true }]))), + // ESLint: ignoreGlobals only skips configured globals, undefined variables still fail + ("undefined_variable", Some(serde_json::json!([{ "ignoreGlobals": true }]))), + ("implicit_global = 1", Some(serde_json::json!([{ "ignoreGlobals": true }]))), + ("export * as snake_cased from 'mod'", None), + ("function foo({ no_camelcased }) {};", None), + ("function foo({ no_camelcased = 'default value' }) {};", None), + ("const no_camelcased = 0; function foo({ camelcased_value = no_camelcased}) {}", None), + ("const { bar: no_camelcased } = foo;", None), + ("function foo({ value_1: my_default }) {}", None), + ("function foo({ isCamelcased: no_camelcased }) {};", None), + ("var { foo: bar_baz = 1 } = quz;", None), + ("const { no_camelcased = false } = bar;", None), + ("const { no_camelcased = foo_bar } = bar;", None), ("not_ignored_foo = 0;", Some(serde_json::json!([{ "allow": ["ignored_bar"] }]))), ("not_ignored_foo = 0;", Some(serde_json::json!([{ "allow": ["_id$"] }]))), ( "foo = { [computed_bar]: 0 };", Some(serde_json::json!([{ "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 6 }, - ("({ a: obj.fo_o } = bar);", None), // { "ecmaVersion": 6 }, - ("({ a: obj.fo_o } = bar);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), // { "ecmaVersion": 6 }, - ("({ a: obj.fo_o.b_ar } = baz);", None), // { "ecmaVersion": 6 }, - ("({ a: { b: { c: obj.fo_o } } } = bar);", None), // { "ecmaVersion": 6 }, - ("({ a: { b: { c: obj.fo_o.b_ar } } } = baz);", None), // { "ecmaVersion": 6 }, - ("([obj.fo_o] = bar);", None), // { "ecmaVersion": 6 }, - ("([obj.fo_o] = bar);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), // { "ecmaVersion": 6 }, - ("([obj.fo_o = 1] = bar);", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 6 }, - ("({ a: [obj.fo_o] } = bar);", None), // { "ecmaVersion": 6 }, - ("({ a: { b: [obj.fo_o] } } = bar);", None), // { "ecmaVersion": 6 }, - ("([obj.fo_o.ba_r] = baz);", None), // { "ecmaVersion": 6 }, - ("({...obj.fo_o} = baz);", None), // { "ecmaVersion": 9 }, - ("({...obj.fo_o.ba_r} = baz);", None), // { "ecmaVersion": 9 }, - ("({c: {...obj.fo_o }} = baz);", None), // { "ecmaVersion": 9 }, - ("obj.o_k.non_camelcase = 0", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 2020 }, - ("(obj?.o_k).non_camelcase = 0", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 2020 }, - ("class C { snake_case; }", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 2022 }, + ), + ("({ a: obj.fo_o } = bar);", None), + ("({ a: obj.fo_o } = bar);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), + ("({ a: obj.fo_o.b_ar } = baz);", None), + ("({ a: { b: { c: obj.fo_o } } } = bar);", None), + ("({ a: { b: { c: obj.fo_o.b_ar } } } = baz);", None), + ("([obj.fo_o] = bar);", None), + ("([obj.fo_o] = bar);", Some(serde_json::json!([{ "ignoreDestructuring": true }]))), + ("([obj.fo_o = 1] = bar);", Some(serde_json::json!([{ "properties": "always" }]))), + ("({ a: [obj.fo_o] } = bar);", None), + ("({ a: { b: [obj.fo_o] } } = bar);", None), + ("([obj.fo_o.ba_r] = baz);", None), + ("({...obj.fo_o} = baz);", None), + ("({...obj.fo_o.ba_r} = baz);", None), + ("({c: {...obj.fo_o }} = baz);", None), + ("obj.o_k.non_camelcase = 0", Some(serde_json::json!([{ "properties": "always" }]))), + ("(obj?.o_k).non_camelcase = 0", Some(serde_json::json!([{ "properties": "always" }]))), + ("class C { snake_case; }", Some(serde_json::json!([{ "properties": "always" }]))), ( "class C { #snake_case; foo() { this.#snake_case; } }", Some(serde_json::json!([{ "properties": "always" }])), - ), // { "ecmaVersion": 2022 }, - ("class C { #snake_case() {} }", Some(serde_json::json!([{ "properties": "always" }]))), // { "ecmaVersion": 2022 }, + ), + ("class C { #snake_case() {} }", Some(serde_json::json!([{ "properties": "always" }]))), ( " const { some_property } = obj; doSomething({ some_property }); ", Some(serde_json::json!([{ "properties": "always", "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 2022 }, + ), ( r#" const { some_property } = obj; @@ -877,7 +1037,7 @@ fn test() { doSomething({ [some_property]: "bar" }); "#, Some(serde_json::json!([{ "properties": "never", "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 2022 }, + ), ( " const { some_property } = obj; @@ -893,20 +1053,124 @@ fn test() { }; ", Some(serde_json::json!([{ "properties": "always", "ignoreDestructuring": true }])), - ), // { "ecmaVersion": 2022 }, + ), ( "import('foo.json', { my_with: { [my_type]: 'json' } })", + Some(serde_json::json!([{ "properties": "always", "ignoreImports": false }])), + ), + ( + "import('foo.json', { my_with: { my_type: my_json } })", + Some(serde_json::json!([{ "properties": "always", "ignoreImports": false }])), + ), + ]; + + Tester::new(Camelcase::NAME, Camelcase::PLUGIN, pass, fail).test_and_snapshot(); + + // Separate test for ignoreGlobals with explicit globals config + // ESLint only skips globals that are explicitly configured + let pass_with_globals = vec![ + ( + "var camelCased = a_global_variable", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some(serde_json::json!({ "globals": { "a_global_variable": "readonly" } })), + ), + ( + "a_global_variable.foo()", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some(serde_json::json!({ "globals": { "a_global_variable": "readonly" } })), + ), + ( + "a_global_variable[undefined]", + Some(serde_json::json!([{ "ignoreGlobals": true }])), Some( - serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + serde_json::json!({ "globals": { "a_global_variable": "readonly", "undefined": "readonly" } }), ), - ), // { "ecmaVersion": 2025 }, + ), ( - "import('foo.json', { my_with: { my_type: my_json } })", + "var foo = a_global_variable.bar", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some(serde_json::json!({ "globals": { "a_global_variable": "readonly" } })), + ), + ( + "a_global_variable.foo = bar", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "writable", "bar": "readonly" } }), + ), + ), + ( + "( { foo: a_global_variable.bar } = baz )", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "readonly", "baz": "readonly" } }), + ), + ), + ( + "a_global_variable = foo", + Some(serde_json::json!([{ "ignoreGlobals": true }])), Some( - serde_json::json!([ { "properties": "always", "ignoreImports": false, }, ]), + serde_json::json!({ "globals": { "a_global_variable": "writable", "foo": "readonly" } }), ), - ), // { "ecmaVersion": 2025 } + ), + ( + "({ a_global_variable } = foo)", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "writable", "foo": "readonly" } }), + ), + ), + ( + "({ snake_cased: a_global_variable } = foo)", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "writable", "foo": "readonly" } }), + ), + ), + ( + "({ snake_cased: a_global_variable = foo } = bar)", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "writable", "foo": "readonly", "bar": "readonly" } }), + ), + ), + ( + "[a_global_variable] = bar", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "writable", "bar": "readonly" } }), + ), + ), + ( + "[a_global_variable = foo] = bar", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "writable", "foo": "readonly", "bar": "readonly" } }), + ), + ), + ( + "foo[a_global_variable] = bar", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "readonly", "foo": "writable", "bar": "readonly" } }), + ), + ), + ( + "var foo = { [a_global_variable]: bar }", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "readonly", "bar": "readonly" } }), + ), + ), + ( + "var { [a_global_variable]: foo } = bar", + Some(serde_json::json!([{ "ignoreGlobals": true }])), + Some( + serde_json::json!({ "globals": { "a_global_variable": "readonly", "bar": "readonly" } }), + ), + ), ]; + let fail_with_globals: Vec<(&str, Option, Option)> = + vec![]; - Tester::new(Camelcase::NAME, Camelcase::PLUGIN, pass, fail).test_and_snapshot(); + Tester::new(Camelcase::NAME, Camelcase::PLUGIN, pass_with_globals, fail_with_globals).test(); } diff --git a/crates/oxc_linter/src/snapshots/eslint_camelcase.snap b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap index b7986ab67517a..1e80d09adab1b 100644 --- a/crates/oxc_linter/src/snapshots/eslint_camelcase.snap +++ b/crates/oxc_linter/src/snapshots/eslint_camelcase.snap @@ -36,6 +36,27 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:2] + 1 │ [foo_bar.baz] + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. + ╭─[camelcase.tsx:1:38] + 1 │ if (foo.bar_baz === boom.bam_pow) { [foo_bar.baz] } + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. + ╭─[camelcase.tsx:1:5] + 1 │ foo.bar_baz = boom.bam_pow + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. ╭─[camelcase.tsx:1:13] 1 │ var foo = { bar_baz: boom.bam_pow } @@ -43,6 +64,20 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ var foo = { bar_baz: boom.bam_pow } + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'boom_pow' is not in camel case. + ╭─[camelcase.tsx:1:9] + 1 │ foo.qux.boom_pow = { bar: boom.bam_pow } + · ──────── + ╰──── + help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. ╭─[camelcase.tsx:1:10] 1 │ var o = {bar_baz: 1} @@ -57,6 +92,41 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'category_alias' is not in camel case. + ╭─[camelcase.tsx:1:20] + 1 │ var { category_id: category_alias } = query; + · ────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'category_alias' is not in camel case. + ╭─[camelcase.tsx:1:20] + 1 │ var { category_id: category_alias } = query; + · ────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'category_id' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ var { [category_id]: categoryId } = query; + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'category_id' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ var { [category_id]: categoryId } = query; + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'other_props' is not in camel case. + ╭─[camelcase.tsx:1:35] + 1 │ var { category_id: categoryId, ...other_props } = query; + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'category_id' is not in camel case. ╭─[camelcase.tsx:1:7] 1 │ var { category_id } = query; @@ -113,6 +183,20 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:27] + 1 │ import { 'snake_cased' as snake_cased } from 'mod' + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'another_snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:27] + 1 │ import { 'snake_cased' as another_snake_cased } from 'mod' + · ─────────────────── + ╰──── + help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. ╭─[camelcase.tsx:1:22] 1 │ import { camelCased, no_camelcased } from "external-module"; @@ -120,6 +204,237 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'another_no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:39] + 1 │ import { no_camelcased as camelCased, another_no_camelcased } from "external-module"; + · ───────────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:22] + 1 │ import camelCased, { no_camelcased } from "external-module"; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ import no_camelcased, { another_no_camelcased as camelCased } from "external-module"; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ import snake_cased from 'mod' + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ import * as snake_cased from 'mod' + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:8] + 1 │ import snake_cased from 'mod' + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ import * as snake_cased from 'mod' + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:18] + 1 │ var camelCased = snake_cased + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ a_global_variable.foo() + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ a_global_variable[undefined] + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:18] + 1 │ var camelCased = snake_cased + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. + ╭─[camelcase.tsx:1:18] + 1 │ var camelCased = snake_cased + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:5] + 1 │ foo.a_global_variable = bar + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ var foo = { a_global_variable: bar } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:32] + 1 │ var foo = { a_global_variable: a_global_variable } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ var foo = { a_global_variable: a_global_variable } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ var foo = { a_global_variable() {} } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ class Foo { a_global_variable() {} } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ a_global_variable: for (;;); + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:16] + 1 │ if (foo) { let a_global_variable; a_global_variable = bar; } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:35] + 1 │ if (foo) { let a_global_variable; a_global_variable = bar; } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:14] + 1 │ function foo(a_global_variable) { foo = a_global_variable; } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:41] + 1 │ function foo(a_global_variable) { foo = a_global_variable; } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:5] + 1 │ var a_global_variable + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:10] + 1 │ function a_global_variable () {} + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:7] + 1 │ const a_global_variable = foo; bar = a_global_variable + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:38] + 1 │ const a_global_variable = foo; bar = a_global_variable + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:30] + 1 │ bar = a_global_variable; var a_global_variable; + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:7] + 1 │ bar = a_global_variable; var a_global_variable; + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ var foo = { a_global_variable } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'a_global_variable' is not in camel case. + ╭─[camelcase.tsx:1:13] + 1 │ var foo = { a_global_variable } + · ───────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'undefined_variable' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ undefined_variable + · ────────────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'implicit_global' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ implicit_global = 1 + · ─────────────── + ╰──── + help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'snake_cased' is not in camel case. ╭─[camelcase.tsx:1:13] 1 │ export * as snake_cased from 'mod' @@ -141,6 +456,20 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:7] + 1 │ const no_camelcased = 0; function foo({ camelcased_value = no_camelcased}) {} + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'camelcased_value' is not in camel case. + ╭─[camelcase.tsx:1:41] + 1 │ const no_camelcased = 0; function foo({ camelcased_value = no_camelcased}) {} + · ──────────────── + ╰──── + help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. ╭─[camelcase.tsx:1:14] 1 │ const { bar: no_camelcased } = foo; @@ -176,6 +505,13 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'no_camelcased' is not in camel case. + ╭─[camelcase.tsx:1:9] + 1 │ const { no_camelcased = foo_bar } = bar; + · ───────────── + ╰──── + help: Rename this identifier to use camelCase. + ⚠ eslint(camelcase): Identifier 'not_ignored_foo' is not in camel case. ╭─[camelcase.tsx:1:1] 1 │ not_ignored_foo = 0; @@ -183,128 +519,215 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. - ╭─[camelcase.tsx:1:11] - 1 │ class C { snake_case; } - · ────────── + ⚠ eslint(camelcase): Identifier 'not_ignored_foo' is not in camel case. + ╭─[camelcase.tsx:1:1] + 1 │ not_ignored_foo = 0; + · ─────────────── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. - ╭─[camelcase.tsx:1:11] - 1 │ class C { #snake_case; foo() { this.#snake_case; } } - · ─────────── + ⚠ eslint(camelcase): Identifier 'computed_bar' is not in camel case. + ╭─[camelcase.tsx:1:10] + 1 │ foo = { [computed_bar]: 0 }; + · ──────────── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. - ╭─[camelcase.tsx:1:37] - 1 │ class C { #snake_case; foo() { this.#snake_case; } } - · ─────────── + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. + ╭─[camelcase.tsx:1:11] + 1 │ ({ a: obj.fo_o } = bar); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. ╭─[camelcase.tsx:1:11] - 1 │ class C { #snake_case() {} } - · ─────────── + 1 │ ({ a: obj.fo_o } = bar); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. - ╭─[camelcase.tsx:1:5] - 1 │ var foo_bar = {}; - · ─────── + ⚠ eslint(camelcase): Identifier 'b_ar' is not in camel case. + ╭─[camelcase.tsx:1:16] + 1 │ ({ a: obj.fo_o.b_ar } = baz); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. - ╭─[camelcase.tsx:1:5] - 1 │ var foo_bar = []; - · ─────── + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. + ╭─[camelcase.tsx:1:21] + 1 │ ({ a: { b: { c: obj.fo_o } } } = bar); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. - ╭─[camelcase.tsx:1:4] - 1 │ ({ foo_bar } = obj); - · ─────── + ⚠ eslint(camelcase): Identifier 'b_ar' is not in camel case. + ╭─[camelcase.tsx:1:26] + 1 │ ({ a: { b: { c: obj.fo_o.b_ar } } } = baz); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. - ╭─[camelcase.tsx:1:9] - 1 │ ({ key: bar_baz } = obj); - · ─────── + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. + ╭─[camelcase.tsx:1:7] + 1 │ ([obj.fo_o] = bar); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'other_name' is not in camel case. - ╭─[camelcase.tsx:1:20] - 1 │ var { category_id: other_name } = query; - · ────────── + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. + ╭─[camelcase.tsx:1:7] + 1 │ ([obj.fo_o] = bar); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'other_name' is not in camel case. - ╭─[camelcase.tsx:1:9] - 1 │ ({ key: other_name } = obj); - · ────────── + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. + ╭─[camelcase.tsx:1:7] + 1 │ ([obj.fo_o = 1] = bar); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'other_snake' is not in camel case. - ╭─[camelcase.tsx:1:24] - 1 │ import { snake_case as other_snake } from "mod"; - · ─────────── + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. + ╭─[camelcase.tsx:1:12] + 1 │ ({ a: [obj.fo_o] } = bar); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'other_props' is not in camel case. + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. ╭─[camelcase.tsx:1:17] - 1 │ const { foo, ...other_props } = obj; - · ─────────── + 1 │ ({ a: { b: [obj.fo_o] } } = bar); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. - ╭─[camelcase.tsx:1:8] - 1 │ const [foo_bar] = arr; - · ─────── + ⚠ eslint(camelcase): Identifier 'ba_r' is not in camel case. + ╭─[camelcase.tsx:1:12] + 1 │ ([obj.fo_o.ba_r] = baz); + · ──── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. + ╭─[camelcase.tsx:1:10] + 1 │ ({...obj.fo_o} = baz); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'second_item' is not in camel case. + ⚠ eslint(camelcase): Identifier 'ba_r' is not in camel case. ╭─[camelcase.tsx:1:15] - 1 │ const [first, second_item] = arr; - · ─────────── + 1 │ ({...obj.fo_o.ba_r} = baz); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. - ╭─[camelcase.tsx:1:8] - 1 │ const [foo_bar = 1] = arr; - · ─────── + ⚠ eslint(camelcase): Identifier 'fo_o' is not in camel case. + ╭─[camelcase.tsx:1:14] + 1 │ ({c: {...obj.fo_o }} = baz); + · ──── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. - ╭─[camelcase.tsx:1:8] - 1 │ const [foo_bar, bar_baz] = arr; - · ─────── + ⚠ eslint(camelcase): Identifier 'non_camelcase' is not in camel case. + ╭─[camelcase.tsx:1:9] + 1 │ obj.o_k.non_camelcase = 0 + · ───────────── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'bar_baz' is not in camel case. - ╭─[camelcase.tsx:1:17] - 1 │ const [foo_bar, bar_baz] = arr; - · ─────── + ⚠ eslint(camelcase): Identifier 'non_camelcase' is not in camel case. + ╭─[camelcase.tsx:1:12] + 1 │ (obj?.o_k).non_camelcase = 0 + · ───────────── ╰──── help: Rename this identifier to use camelCase. - ⚠ eslint(camelcase): Identifier 'foo_bar' is not in camel case. - ╭─[camelcase.tsx:1:8] - 1 │ const [foo_bar = 1] = arr; - · ─────── + ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. + ╭─[camelcase.tsx:1:11] + 1 │ class C { snake_case; } + · ────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. + ╭─[camelcase.tsx:1:11] + 1 │ class C { #snake_case; foo() { this.#snake_case; } } + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. + ╭─[camelcase.tsx:1:37] + 1 │ class C { #snake_case; foo() { this.#snake_case; } } + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'snake_case' is not in camel case. + ╭─[camelcase.tsx:1:11] + 1 │ class C { #snake_case() {} } + · ─────────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'some_property' is not in camel case. + ╭─[camelcase.tsx:3:30] + 2 │ const { some_property } = obj; + 3 │ doSomething({ some_property }); + · ───────────── + 4 │ + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'some_property' is not in camel case. + ╭─[camelcase.tsx:4:31] + 3 │ doSomething({ some_property }); + 4 │ doSomething({ [some_property]: "bar" }); + · ───────────── + 5 │ + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'some_property' is not in camel case. + ╭─[camelcase.tsx:4:30] + 3 │ + 4 │ const bar = { some_property }; + · ───────────── + 5 │ + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'some_property' is not in camel case. + ╭─[camelcase.tsx:6:20] + 5 │ + 6 │ obj.some_property = 10; + · ───────────── + 7 │ + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'some_property' is not in camel case. + ╭─[camelcase.tsx:8:30] + 7 │ + 8 │ const xyz = { some_property: obj.some_property }; + · ───────────── + 9 │ + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'my_type' is not in camel case. + ╭─[camelcase.tsx:1:34] + 1 │ import('foo.json', { my_with: { [my_type]: 'json' } }) + · ─────── + ╰──── + help: Rename this identifier to use camelCase. + + ⚠ eslint(camelcase): Identifier 'my_json' is not in camel case. + ╭─[camelcase.tsx:1:42] + 1 │ import('foo.json', { my_with: { my_type: my_json } }) + · ─────── ╰──── help: Rename this identifier to use camelCase. From 8f6f41578c209c4837f17f25fba8ae42174e413a Mon Sep 17 00:00:00 2001 From: Tu Shaokun <2801884530@qq.com> Date: Wed, 17 Dec 2025 15:23:55 +0800 Subject: [PATCH 8/8] chore: add fo_o and ba_r to typos ignore list (test identifiers) --- .typos.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.typos.toml b/.typos.toml index 5e559f1943cb9..01a65d8d28968 100644 --- a/.typos.toml +++ b/.typos.toml @@ -50,3 +50,5 @@ inferrable = "inferrable" [default.extend-identifiers] IIFEs = "IIFEs" allowIIFEs = "allowIIFEs" +fo_o = "fo_o" +ba_r = "ba_r"