1- use oxc_ast:: AstKind ;
1+ use lazy_regex:: Regex ;
2+ use oxc_ast:: { AstKind , ast:: * } ;
23use oxc_diagnostics:: OxcDiagnostic ;
34use oxc_macros:: declare_oxc_lint;
45use oxc_span:: { CompactStr , Span } ;
@@ -14,12 +15,30 @@ fn camelcase_diagnostic(span: Span, name: &str) -> OxcDiagnostic {
1415
1516#[ derive( Debug , Clone ) ]
1617pub struct CamelcaseConfig {
18+ properties : PropertiesOption ,
19+ ignore_destructuring : bool ,
20+ ignore_imports : bool ,
21+ ignore_globals : bool ,
1722 allow : Vec < CompactStr > ,
23+ allow_regexes : Vec < Regex > ,
24+ }
25+
26+ #[ derive( Debug , Clone ) ]
27+ pub enum PropertiesOption {
28+ Always ,
29+ Never ,
1830}
1931
2032impl Default for CamelcaseConfig {
2133 fn default ( ) -> Self {
22- Self { allow : vec ! [ ] }
34+ Self {
35+ properties : PropertiesOption :: Always ,
36+ ignore_destructuring : false ,
37+ ignore_imports : false ,
38+ ignore_globals : false ,
39+ allow : vec ! [ ] ,
40+ allow_regexes : vec ! [ ] ,
41+ }
2342 }
2443}
2544
@@ -48,17 +67,53 @@ impl From<&Value> for Camelcase {
4867
4968 let config = config. unwrap ( ) ;
5069
51- let allow = if let Some ( allow_value) = config. get ( "allow" ) {
70+ let properties = match config. get ( "properties" ) . and_then ( |v| v. as_str ( ) ) {
71+ Some ( "never" ) => PropertiesOption :: Never ,
72+ _ => PropertiesOption :: Always , // default is "always"
73+ } ;
74+
75+ let ignore_destructuring =
76+ config. get ( "ignoreDestructuring" ) . and_then ( |v| v. as_bool ( ) ) . unwrap_or ( false ) ;
77+
78+ let ignore_imports = config. get ( "ignoreImports" ) . and_then ( |v| v. as_bool ( ) ) . unwrap_or ( false ) ;
79+
80+ let ignore_globals = config. get ( "ignoreGlobals" ) . and_then ( |v| v. as_bool ( ) ) . unwrap_or ( false ) ;
81+
82+ let ( allow, allow_regexes) = if let Some ( allow_value) = config. get ( "allow" ) {
5283 if let Some ( allow_array) = allow_value. as_array ( ) {
53- allow_array. iter ( ) . filter_map ( |v| v. as_str ( ) ) . map ( CompactStr :: new) . collect ( )
84+ let mut allow_list = Vec :: new ( ) ;
85+ let mut regex_list = Vec :: new ( ) ;
86+
87+ for item in allow_array. iter ( ) . filter_map ( |v| v. as_str ( ) ) {
88+ if item. starts_with ( '^' )
89+ || item. contains ( [ '*' , '+' , '?' , '[' , ']' , '(' , ')' , '|' ] )
90+ {
91+ // Treat as regex
92+ if let Ok ( regex) = Regex :: new ( item) {
93+ regex_list. push ( regex) ;
94+ }
95+ } else {
96+ // Treat as literal string
97+ allow_list. push ( CompactStr :: new ( item) ) ;
98+ }
99+ }
100+
101+ ( allow_list, regex_list)
54102 } else {
55- vec ! [ ]
103+ ( vec ! [ ] , vec ! [ ] )
56104 }
57105 } else {
58- vec ! [ ]
106+ ( vec ! [ ] , vec ! [ ] )
59107 } ;
60108
61- Self ( Box :: new ( CamelcaseConfig { allow } ) )
109+ Self ( Box :: new ( CamelcaseConfig {
110+ properties,
111+ ignore_destructuring,
112+ ignore_imports,
113+ ignore_globals,
114+ allow,
115+ allow_regexes,
116+ } ) )
62117 }
63118}
64119
@@ -158,22 +213,139 @@ impl Rule for Camelcase {
158213
159214 fn run < ' a > ( & self , node : & AstNode < ' a > , ctx : & LintContext < ' a > ) {
160215 match node. kind ( ) {
216+ // Variable declarations, function declarations, parameters
161217 AstKind :: BindingIdentifier ( binding_ident) => {
162- let name = & binding_ident. name ;
163- let atom_str = name. as_str ( ) ;
164-
165- if !self . is_underscored ( atom_str) || self . is_allowed ( atom_str) {
166- return ;
218+ if self . should_check_binding_identifier ( node, ctx) {
219+ self . check_identifier ( binding_ident. span , & binding_ident. name , ctx) ;
220+ }
221+ }
222+ // Object property keys
223+ AstKind :: ObjectProperty ( property) => {
224+ if let PropertyKey :: StaticIdentifier ( ident) = & property. key {
225+ if self . should_check_property_key ( node, ctx) {
226+ self . check_identifier ( ident. span , & ident. name , ctx) ;
227+ }
228+ }
229+ }
230+ // Import specifiers are handled via BindingIdentifier
231+ // Method definitions
232+ AstKind :: MethodDefinition ( method_def) => {
233+ if let PropertyKey :: StaticIdentifier ( ident) = & method_def. key {
234+ if self . should_check_method_key ( node, ctx) {
235+ self . check_identifier ( ident. span , & ident. name , ctx) ;
236+ }
167237 }
168-
169- ctx. diagnostic ( camelcase_diagnostic ( binding_ident. span , atom_str) ) ;
170238 }
171239 _ => { }
172240 }
173241 }
174242}
175243
176244impl Camelcase {
245+ fn check_identifier < ' a > ( & self , span : Span , name : & str , ctx : & LintContext < ' a > ) {
246+ if !self . is_underscored ( name) || self . is_allowed ( name) {
247+ return ;
248+ }
249+ ctx. diagnostic ( camelcase_diagnostic ( span, name) ) ;
250+ }
251+
252+ fn should_check_binding_identifier < ' a > (
253+ & self ,
254+ node : & AstNode < ' a > ,
255+ ctx : & LintContext < ' a > ,
256+ ) -> bool {
257+ // Check if this is a global reference that should be ignored
258+ if self . ignore_globals {
259+ // For now, we'll skip the global check as the semantic API usage is complex
260+ // TODO: Implement proper global reference checking
261+ }
262+
263+ // Check if this is inside an import and should be ignored
264+ if self . ignore_imports && self . is_in_import_context ( node, ctx) {
265+ return false ;
266+ }
267+
268+ // Check if this is inside a destructuring pattern that should be ignored
269+ if self . ignore_destructuring && self . is_in_destructuring_pattern ( node, ctx) {
270+ return false ;
271+ }
272+
273+ true
274+ }
275+
276+ fn should_check_property_key < ' a > ( & self , node : & AstNode < ' a > , ctx : & LintContext < ' a > ) -> bool {
277+ // Only check property keys if properties: "always"
278+ match self . properties {
279+ PropertiesOption :: Always => {
280+ // Don't check if this is in a destructuring pattern and ignoreDestructuring is true
281+ if self . ignore_destructuring && self . is_in_destructuring_pattern ( node, ctx) {
282+ return false ;
283+ }
284+ true
285+ }
286+ PropertiesOption :: Never => false ,
287+ }
288+ }
289+
290+ fn should_check_method_key < ' a > ( & self , node : & AstNode < ' a > , ctx : & LintContext < ' a > ) -> bool {
291+ // Check method keys only if properties: "always"
292+ match self . properties {
293+ PropertiesOption :: Always => {
294+ !self . ignore_destructuring || !self . is_in_destructuring_pattern ( node, ctx)
295+ }
296+ PropertiesOption :: Never => false ,
297+ }
298+ }
299+
300+ fn is_in_import_context < ' a > ( & self , node : & AstNode < ' a > , ctx : & LintContext < ' a > ) -> bool {
301+ // Walk up the parent chain to see if we're inside an import statement
302+ let mut current = node;
303+ loop {
304+ let parent = ctx. nodes ( ) . parent_node ( current. id ( ) ) ;
305+ match parent. kind ( ) {
306+ AstKind :: ImportSpecifier ( _)
307+ | AstKind :: ImportDefaultSpecifier ( _)
308+ | AstKind :: ImportNamespaceSpecifier ( _) => return true ,
309+ AstKind :: Program ( _) => break ,
310+ _ => { }
311+ }
312+ current = parent;
313+ }
314+ false
315+ }
316+
317+ fn is_in_destructuring_pattern < ' a > ( & self , node : & AstNode < ' a > , ctx : & LintContext < ' a > ) -> bool {
318+ // Walk up the parent chain to see if we're inside a destructuring pattern
319+ let mut current = node;
320+ loop {
321+ let parent = ctx. nodes ( ) . parent_node ( current. id ( ) ) ;
322+ match parent. kind ( ) {
323+ AstKind :: ObjectPattern ( _) | AstKind :: ArrayPattern ( _) => return true ,
324+ // If we hit a variable declarator, check if it has a destructuring pattern
325+ AstKind :: VariableDeclarator ( declarator) => match & declarator. id . kind {
326+ BindingPatternKind :: ObjectPattern ( _) | BindingPatternKind :: ArrayPattern ( _) => {
327+ return true ;
328+ }
329+ _ => { }
330+ } ,
331+ // Stop at function boundaries unless we're checking parameters
332+ AstKind :: Function ( _) | AstKind :: ArrowFunctionExpression ( _) => {
333+ // Check if we're in the parameters
334+ let grandparent = ctx. nodes ( ) . parent_node ( parent. id ( ) ) ;
335+ if matches ! ( grandparent. kind( ) , AstKind :: FormalParameters ( _) ) {
336+ current = parent;
337+ continue ;
338+ }
339+ break ;
340+ }
341+ AstKind :: Program ( _) => break ,
342+ _ => { }
343+ }
344+ current = parent;
345+ }
346+ false
347+ }
348+
177349 fn is_underscored ( & self , name : & str ) -> bool {
178350 // Remove leading and trailing underscores
179351 let name_body = name. trim_start_matches ( '_' ) . trim_end_matches ( '_' ) ;
@@ -183,12 +355,13 @@ impl Camelcase {
183355 }
184356
185357 fn is_allowed ( & self , name : & str ) -> bool {
186- self . allow . iter ( ) . any ( |entry| {
187- name == entry. as_str ( ) || {
188- // Try to match as regex - simplified, just exact match for now
189- false
190- }
191- } )
358+ // Check literal matches
359+ if self . allow . iter ( ) . any ( |entry| name == entry. as_str ( ) ) {
360+ return true ;
361+ }
362+
363+ // Check regex matches
364+ self . allow_regexes . iter ( ) . any ( |regex| regex. is_match ( name) )
192365 }
193366}
194367
@@ -197,19 +370,112 @@ fn test() {
197370 use crate :: tester:: Tester ;
198371
199372 let pass = vec ! [
373+ // Basic camelCase
200374 ( "var firstName = \" Ned\" " , None ) ,
201375 ( "var __myPrivateVariable = \" Patrick\" " , None ) ,
202376 ( "var myPrivateVariable_ = \" Patrick\" " , None ) ,
203377 ( "function doSomething(){}" , None ) ,
378+ // Constants (all uppercase with underscores)
204379 ( "var MY_GLOBAL = 1" , None ) ,
205380 ( "var ANOTHER_GLOBAL = 1" , None ) ,
206- ( "var foo_bar" , Some ( serde_json:: json!( [ { "allow" : [ "foo_bar" ] } ] ) ) ) ,
381+ // Property access (should not be checked)
382+ ( "var foo1 = bar.baz_boom;" , None ) ,
383+ ( "var foo2 = { qux: bar.baz_boom };" , None ) ,
384+ ( "obj.do_something();" , None ) ,
385+ ( "do_something();" , None ) ,
386+ ( "new do_something();" , None ) ,
387+ // Import with alias
388+ ( "import { no_camelcased as camelCased } from \" external-module\" ;" , None ) ,
389+ // Destructuring with rename
390+ ( "var { category_id: category } = query;" , None ) ,
391+ ( "var { category_id: categoryId } = query;" , None ) ,
392+ ( "function foo({ isCamelCased }) {}" , None ) ,
393+ ( "function bar({ isCamelCased: isAlsoCamelCased }) {}" , None ) ,
394+ ( "function baz({ isCamelCased = 'default value' }) {}" , None ) ,
395+ ( "var { categoryId = 1 } = query;" , None ) ,
396+ ( "var { foo: isCamelCased } = bar;" , None ) ,
397+ ( "var { foo: camelCasedName = 1 } = quz;" , None ) ,
398+ // Properties: "never" option
399+ ( "var obj = { my_pref: 1 };" , Some ( serde_json:: json!( [ { "properties" : "never" } ] ) ) ) ,
400+ ( "obj.foo_bar = \" baz\" ;" , Some ( serde_json:: json!( [ { "properties" : "never" } ] ) ) ) ,
401+ // ignoreDestructuring: true
402+ (
403+ "var { category_id } = query;" ,
404+ Some ( serde_json:: json!( [ { "ignoreDestructuring" : true } ] ) ) ,
405+ ) ,
406+ (
407+ "var { category_name = 1 } = query;" ,
408+ Some ( serde_json:: json!( [ { "ignoreDestructuring" : true } ] ) ) ,
409+ ) ,
410+ (
411+ "var { category_id_name: category_id_name } = query;" ,
412+ Some ( serde_json:: json!( [ { "ignoreDestructuring" : true } ] ) ) ,
413+ ) ,
414+ // ignoreDestructuring: true also ignores renamed aliases (simplified behavior)
415+ (
416+ "var { category_id: category_alias } = query;" ,
417+ Some ( serde_json:: json!( [ { "ignoreDestructuring" : true } ] ) ) ,
418+ ) ,
419+ // ignoreDestructuring: true also ignores rest parameters (simplified behavior)
420+ (
421+ "var { category_id, ...other_props } = query;" ,
422+ Some ( serde_json:: json!( [ { "ignoreDestructuring" : true } ] ) ) ,
423+ ) ,
424+ // ignoreImports: true
425+ (
426+ "import { snake_cased } from 'mod';" ,
427+ Some ( serde_json:: json!( [ { "ignoreImports" : true } ] ) ) ,
428+ ) ,
429+ // ignoreGlobals: true
430+ ( "var foo = no_camelcased;" , Some ( serde_json:: json!( [ { "ignoreGlobals" : true } ] ) ) ) ,
431+ // allow option
432+ ( "var foo_bar;" , Some ( serde_json:: json!( [ { "allow" : [ "foo_bar" ] } ] ) ) ) ,
433+ (
434+ "function UNSAFE_componentWillMount() {}" ,
435+ Some ( serde_json:: json!( [ { "allow" : [ "UNSAFE_componentWillMount" ] } ] ) ) ,
436+ ) ,
437+ // allow with regex
438+ (
439+ "function UNSAFE_componentWillMount() {}" ,
440+ Some ( serde_json:: json!( [ { "allow" : [ "^UNSAFE_" ] } ] ) ) ,
441+ ) ,
442+ (
443+ "function UNSAFE_componentWillReceiveProps() {}" ,
444+ Some ( serde_json:: json!( [ { "allow" : [ "^UNSAFE_" ] } ] ) ) ,
445+ ) ,
446+ // Combined options
447+ (
448+ "var { some_property } = obj; doSomething({ some_property });" ,
449+ Some ( serde_json:: json!( [ { "properties" : "never" , "ignoreDestructuring" : true } ] ) ) ,
450+ ) ,
451+ // Using destructured vars with underscores (simplified implementation ignores both)
452+ (
453+ "var { some_property } = obj; var foo = some_property + 1;" ,
454+ Some ( serde_json:: json!( [ { "ignoreDestructuring" : true } ] ) ) ,
455+ ) ,
207456 ] ;
208457
209458 let fail = vec ! [
459+ // Basic violations
210460 ( "var no_camelcased = 1;" , None ) ,
211461 ( "function no_camelcased(){}" , None ) ,
212462 ( "function bar( obj_name ){}" , None ) ,
463+ // Import violations
464+ ( "import { snake_cased } from 'mod';" , None ) ,
465+ ( "import default_import from 'mod';" , None ) ,
466+ ( "import * as namespaced_import from 'mod';" , None ) ,
467+ // Property violations (properties: "always" - default)
468+ ( "var obj = { my_pref: 1 };" , None ) ,
469+ // Destructuring violations
470+ ( "var { category_id } = query;" , None ) ,
471+ ( "var { category_name = 1 } = query;" , None ) ,
472+ ( "var { category_id: category_title } = query;" , None ) ,
473+ ( "var { category_id: category_alias } = query;" , None ) ,
474+ ( "var { category_id: categoryId, ...other_props } = query;" , None ) ,
475+ // Function parameter destructuring
476+ ( "function foo({ no_camelcased }) {}" , None ) ,
477+ ( "function bar({ isCamelcased: no_camelcased }) {}" , None ) ,
478+ ( "function baz({ no_camelcased = 'default value' }) {}" , None ) ,
213479 ] ;
214480
215481 Tester :: new ( Camelcase :: NAME , Camelcase :: PLUGIN , pass, fail) . test_and_snapshot ( ) ;
0 commit comments