eslint-plugin-code-style 1.13.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.14.1] - 2026-02-05
11
+
12
+ ### Enhanced
13
+
14
+ - **`function-naming-convention`** - Detect functions destructured from hooks without proper naming
15
+ - Flags: `const { logout } = useAuth()` (should be `logoutHandler`)
16
+ - Auto-fixes to: `const { logout: logoutHandler } = useAuth()`
17
+ - Renames all usages of the local variable
18
+ - Only flags clear action verbs (login, logout, toggle, increment, etc.)
19
+
20
+ ---
21
+
22
+ ## [1.14.0] - 2026-02-05
23
+
24
+ **New Rule: useState Naming Convention**
25
+
26
+ **Version Range:** v1.13.0 → v1.14.0
27
+
28
+ ### Added
29
+
30
+ **New Rules (1)**
31
+ - `use-state-naming-convention` - Enforce boolean useState variables to start with valid prefixes 🔧
32
+ - Boolean state must start with: `is`, `has`, `with`, `without` (configurable)
33
+ - Auto-fixes both state variable and setter function names, plus all usages
34
+ - Detects boolean literals (`useState(false)`) and type annotations (`useState<boolean>()`)
35
+ - Options: `booleanPrefixes`, `extendBooleanPrefixes`, `allowPastVerbBoolean`, `allowContinuousVerbBoolean`
36
+
37
+ ### Enhanced
38
+
39
+ - **`folder-component-suffix`** - Add auto-fix to rename component and all references in the file
40
+ - **`function-naming-convention`** - Detect useCallback-wrapped functions in custom hooks
41
+ - **`prop-naming-convention`** - Auto-fix now renames both type annotation AND destructured parameter with all usages
42
+
43
+ ### Stats
44
+
45
+ - Total Rules: 76 (was 75)
46
+ - Auto-fixable: 67 rules 🔧
47
+ - Configurable: 17 rules ⚙️
48
+ - Report-only: 9 rules
49
+
50
+ **Full Changelog:** [v1.13.0...v1.14.0](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.13.0...v1.14.0)
51
+
52
+ ---
53
+
10
54
  ## [1.13.0] - 2026-02-05
11
55
 
12
56
  **New Rule: Prop Naming Convention & Auto-Fix Enhancements**
@@ -1622,6 +1666,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1622
1666
 
1623
1667
  ---
1624
1668
 
1669
+ [1.14.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.14.0...v1.14.1
1670
+ [1.14.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.13.0...v1.14.0
1625
1671
  [1.13.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.12.1...v1.13.0
1626
1672
  [1.12.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.12.0...v1.12.1
1627
1673
  [1.12.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.9...v1.12.0
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  **A powerful ESLint plugin for enforcing consistent code formatting and style rules in React/JSX projects.**
21
21
 
22
- *75 rules (66 auto-fixable, 17 configurable) to keep your codebase clean and consistent*
22
+ *76 rules (67 auto-fixable, 17 configurable) to keep your codebase clean and consistent*
23
23
 
24
24
  </div>
25
25
 
@@ -36,7 +36,7 @@ This plugin provides **75 custom rules** (66 auto-fixable, 17 configurable) for
36
36
  - **Works alongside existing tools** — Complements ESLint's built-in rules and packages like eslint-plugin-react, eslint-plugin-import, etc
37
37
  - **Self-sufficient rules** — Each rule handles complete formatting independently
38
38
  - **Consistency at scale** — Reduces code-style differences between team members by enforcing uniform formatting across your projects
39
- - **Highly automated** — 66 of 75 rules support auto-fix with `eslint --fix`
39
+ - **Highly automated** — 67 of 76 rules support auto-fix with `eslint --fix`
40
40
 
41
41
  When combined with ESLint's native rules and other popular plugins, this package helps create a complete code style solution that keeps your codebase clean and consistent.
42
42
 
@@ -97,7 +97,7 @@ We provide **ready-to-use ESLint flat configuration files** that combine `eslint
97
97
  <td width="50%">
98
98
 
99
99
  ### 🔧 Auto-Fixable Rules
100
- **66 rules** support automatic fixing with `eslint --fix`. **17 rules** have configurable options. 9 rules are report-only (require manual changes).
100
+ **67 rules** support automatic fixing with `eslint --fix`. **17 rules** have configurable options. 9 rules are report-only (require manual changes).
101
101
 
102
102
  </td>
103
103
  <td width="50%">
@@ -214,6 +214,7 @@ rules: {
214
214
  "code-style/function-params-per-line": "error",
215
215
  "code-style/hook-callback-format": "error",
216
216
  "code-style/hook-deps-per-line": "error",
217
+ "code-style/use-state-naming-convention": "error",
217
218
  "code-style/if-else-spacing": "error",
218
219
  "code-style/if-statement-format": "error",
219
220
  "code-style/import-format": "error",
@@ -265,7 +266,7 @@ rules: {
265
266
 
266
267
  ## 📖 Rules Categories
267
268
 
268
- > **75 rules total** — 66 with auto-fix 🔧, 17 configurable ⚙️, 9 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
269
+ > **76 rules total** — 67 with auto-fix 🔧, 17 configurable ⚙️, 9 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
269
270
  >
270
271
  > **Legend:** 🔧 Auto-fixable with `eslint --fix` • ⚙️ Customizable options
271
272
 
@@ -316,6 +317,7 @@ rules: {
316
317
  | **Hook Rules** | |
317
318
  | `hook-callback-format` | React hooks: callback on new line, deps array on separate line, proper indentation 🔧 |
318
319
  | `hook-deps-per-line` | Collapse deps ≤ threshold to one line; expand larger arrays with each dep on own line (default: >2) 🔧 ⚙️ |
320
+ | `use-state-naming-convention` | Boolean useState variables must start with is/has/with/without prefix 🔧 ⚙️ |
319
321
  | **Import/Export Rules** | |
320
322
  | `absolute-imports-only` | Use alias imports from index files only (not deep paths), no relative imports (default: `@/`) ⚙️ |
321
323
  | `export-format` | `export {` on same line; collapse ≤ threshold to one line; expand larger with each specifier on own line (default: ≤3) 🔧 ⚙️ |
@@ -1761,6 +1763,49 @@ useEffect(() => {}, [
1761
1763
 
1762
1764
  <br />
1763
1765
 
1766
+ ### `use-state-naming-convention`
1767
+
1768
+ **What it does:** Enforces boolean useState variables to start with valid prefixes (is, has, with, without).
1769
+
1770
+ **Why use it:** Consistent boolean state naming makes code more predictable and self-documenting. When you see `isLoading`, you immediately know it's a boolean state.
1771
+
1772
+ ```typescript
1773
+ // ✅ Good — boolean state with proper prefix
1774
+ const [isLoading, setIsLoading] = useState(false);
1775
+ const [hasError, setHasError] = useState<boolean>(false);
1776
+ const [isAuthenticated, setIsAuthenticated] = useState(true);
1777
+ const [withBorder, setWithBorder] = useState(false);
1778
+
1779
+ // ❌ Bad — boolean state without prefix
1780
+ const [loading, setLoading] = useState(false);
1781
+ const [authenticated, setAuthenticated] = useState<boolean>(true);
1782
+ const [error, setError] = useState<boolean>(false);
1783
+ ```
1784
+
1785
+ **Customization Options:**
1786
+
1787
+ | Option | Type | Default | Description |
1788
+ |--------|------|---------|-------------|
1789
+ | `booleanPrefixes` | `string[]` | `["is", "has", "with", "without"]` | Replace default prefixes entirely |
1790
+ | `extendBooleanPrefixes` | `string[]` | `[]` | Add additional prefixes to defaults |
1791
+ | `allowPastVerbBoolean` | `boolean` | `false` | Allow past verb names without prefix (disabled, selected) |
1792
+ | `allowContinuousVerbBoolean` | `boolean` | `false` | Allow continuous verb names without prefix (loading, saving) |
1793
+
1794
+ ```javascript
1795
+ // Example: Allow "loading" and "disabled" without prefix
1796
+ "code-style/use-state-naming-convention": ["error", {
1797
+ allowPastVerbBoolean: true,
1798
+ allowContinuousVerbBoolean: true
1799
+ }]
1800
+
1801
+ // Example: Add "should" prefix
1802
+ "code-style/use-state-naming-convention": ["error", {
1803
+ extendBooleanPrefixes: ["should"]
1804
+ }]
1805
+ ```
1806
+
1807
+ <br />
1808
+
1764
1809
  ## 📥 Import/Export Rules
1765
1810
 
1766
1811
  ### `absolute-imports-only`
@@ -3849,7 +3894,7 @@ const UseAuth = () => {}; // hooks should be camelCase
3849
3894
 
3850
3895
  ## 🔧 Auto-fixing
3851
3896
 
3852
- 66 of 75 rules support auto-fixing. Run ESLint with the `--fix` flag:
3897
+ 67 of 76 rules support auto-fixing. Run ESLint with the `--fix` flag:
3853
3898
 
3854
3899
  ```bash
3855
3900
  # Fix all files in src directory
package/index.d.ts CHANGED
@@ -33,6 +33,7 @@ export type RuleNames =
33
33
  | "code-style/function-params-per-line"
34
34
  | "code-style/hook-callback-format"
35
35
  | "code-style/hook-deps-per-line"
36
+ | "code-style/use-state-naming-convention"
36
37
  | "code-style/if-else-spacing"
37
38
  | "code-style/if-statement-format"
38
39
  | "code-style/import-format"
package/index.js CHANGED
@@ -2115,6 +2115,7 @@ const functionNamingConvention = {
2115
2115
  "build", "make", "generate", "compute", "calculate", "process", "execute", "run",
2116
2116
  "evaluate", "analyze", "measure", "benchmark", "profile", "optimize",
2117
2117
  "count", "sum", "avg", "min", "max", "clamp", "round", "floor", "ceil", "abs",
2118
+ "increment", "decrement", "multiply", "divide", "mod", "negate",
2118
2119
  // Invocation
2119
2120
  "apply", "call", "invoke", "trigger", "fire", "dispatch", "emit", "raise", "signal",
2120
2121
  // Auth
@@ -2216,6 +2217,23 @@ const functionNamingConvention = {
2216
2217
  if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id) {
2217
2218
  name = node.parent.id.name;
2218
2219
  identifierNode = node.parent.id;
2220
+ } else if (node.parent && node.parent.type === "CallExpression") {
2221
+ // Check for useCallback wrapped functions: const login = useCallback(() => {...}, [])
2222
+ // Note: Skip useMemo - it returns computed values, not action functions
2223
+ const callExpr = node.parent;
2224
+ const callee = callExpr.callee;
2225
+
2226
+ // Only check useCallback (not useMemo, useEffect, etc.)
2227
+ if (callee && callee.type === "Identifier" && callee.name === "useCallback") {
2228
+ // Check if the function is the first argument to useCallback
2229
+ if (callExpr.arguments && callExpr.arguments[0] === node) {
2230
+ // Check if the CallExpression is the init of a VariableDeclarator
2231
+ if (callExpr.parent && callExpr.parent.type === "VariableDeclarator" && callExpr.parent.id) {
2232
+ name = callExpr.parent.id.name;
2233
+ identifierNode = callExpr.parent.id;
2234
+ }
2235
+ }
2236
+ }
2219
2237
  }
2220
2238
  }
2221
2239
 
@@ -2321,15 +2339,14 @@ const functionNamingConvention = {
2321
2339
  if (!hasVerbPrefix && !hasHandlerSuffix) {
2322
2340
  context.report({
2323
2341
  message: `Function "${name}" should start with a verb (get, set, fetch, etc.) AND end with "Handler" (e.g., getDataHandler, clickHandler)`,
2324
- node: node.id || node.parent.id,
2342
+ node: identifierNode,
2325
2343
  });
2326
2344
  } else if (!hasVerbPrefix) {
2327
2345
  context.report({
2328
2346
  message: `Function "${name}" should start with a verb (get, set, fetch, handle, click, submit, etc.)`,
2329
- node: node.id || node.parent.id,
2347
+ node: identifierNode,
2330
2348
  });
2331
2349
  } else if (!hasHandlerSuffix) {
2332
- const identifierNode = node.id || node.parent.id;
2333
2350
  const newName = `${name}Handler`;
2334
2351
 
2335
2352
  context.report({
@@ -2483,11 +2500,139 @@ const functionNamingConvention = {
2483
2500
  }
2484
2501
  };
2485
2502
 
2503
+ // Check functions destructured from custom hooks: const { logout } = useAuth()
2504
+ // Valid: onLogout, logoutAction, logoutHandler
2505
+ // Invalid: logout (should be logoutHandler)
2506
+ const checkDestructuredHookFunctionHandler = (node) => {
2507
+ // Check if this is destructuring from a hook call: const { ... } = useXxx()
2508
+ if (!node.init || node.init.type !== "CallExpression") return;
2509
+ if (!node.id || node.id.type !== "ObjectPattern") return;
2510
+
2511
+ const callee = node.init.callee;
2512
+
2513
+ // Check if it's a hook call (starts with "use")
2514
+ if (!callee || callee.type !== "Identifier" || !hookRegex.test(callee.name)) return;
2515
+
2516
+ // Action verbs that are clearly function names (not noun-like)
2517
+ // These are verbs that when used alone as a name, clearly indicate an action/function
2518
+ const actionVerbs = [
2519
+ // Auth actions
2520
+ "login", "logout", "authenticate", "authorize", "signup", "signout", "signin",
2521
+ // CRUD actions (when standalone, not as prefix)
2522
+ "submit", "reset", "clear", "refresh", "reload", "retry",
2523
+ // Toggle/state actions
2524
+ "toggle", "enable", "disable", "activate", "deactivate",
2525
+ "show", "hide", "open", "close", "expand", "collapse",
2526
+ // Navigation
2527
+ "navigate", "redirect", "goBack", "goForward",
2528
+ // Increment/decrement
2529
+ "increment", "decrement", "increase", "decrease",
2530
+ // Other common hook function names
2531
+ "connect", "disconnect", "subscribe", "unsubscribe",
2532
+ "start", "stop", "pause", "resume", "cancel", "abort",
2533
+ "select", "deselect", "check", "uncheck",
2534
+ "add", "remove", "insert", "delete", "update",
2535
+ ];
2536
+
2537
+ // Check each destructured property
2538
+ node.id.properties.forEach((prop) => {
2539
+ if (prop.type !== "Property" || prop.key.type !== "Identifier") return;
2540
+
2541
+ const name = prop.key.name;
2542
+
2543
+ // Get the local variable name (value node for non-shorthand, key for shorthand)
2544
+ const valueNode = prop.value || prop.key;
2545
+ const localName = valueNode.type === "Identifier" ? valueNode.name : name;
2546
+
2547
+ // Skip if local name starts with "on" (like onLogout, onClick)
2548
+ if (/^on[A-Z]/.test(localName)) return;
2549
+
2550
+ // Skip if local name ends with "Action" (like logoutAction)
2551
+ if (/Action$/.test(localName)) return;
2552
+
2553
+ // Skip if local name ends with "Handler" (like logoutHandler)
2554
+ if (handlerRegex.test(localName)) return;
2555
+
2556
+ // Skip boolean-like names (is, has, can, should, etc.)
2557
+ const booleanPrefixes = ["is", "has", "can", "should", "will", "did", "was", "were", "does"];
2558
+
2559
+ if (booleanPrefixes.some((prefix) => localName.startsWith(prefix) && localName.length > prefix.length && /[A-Z]/.test(localName[prefix.length]))) return;
2560
+
2561
+ // Only flag if the name is exactly an action verb or starts with one followed by uppercase
2562
+ const isActionVerb = actionVerbs.some((verb) =>
2563
+ localName === verb || (localName.startsWith(verb) && localName.length > verb.length && /[A-Z]/.test(localName[verb.length])));
2564
+
2565
+ if (!isActionVerb) return;
2566
+
2567
+ // This is a function name without proper prefix/suffix
2568
+ // Add Handler suffix: logout -> logoutHandler
2569
+ const suggestedName = localName + "Handler";
2570
+
2571
+ context.report({
2572
+ fix(fixer) {
2573
+ const fixes = [];
2574
+ const sourceCode = context.sourceCode || context.getSourceCode();
2575
+
2576
+ if (prop.shorthand) {
2577
+ // Shorthand: { logout } -> { logout: logoutHandler }
2578
+ // Replace the entire property with key: newValue format
2579
+ fixes.push(fixer.replaceText(prop, `${name}: ${suggestedName}`));
2580
+ } else {
2581
+ // Non-shorthand: { logout: myVar } -> { logout: myVarHandler }
2582
+ // Just replace the value identifier
2583
+ fixes.push(fixer.replaceText(valueNode, suggestedName));
2584
+ }
2585
+
2586
+ // Find and rename all usages of the local variable
2587
+ const scope = sourceCode.getScope
2588
+ ? sourceCode.getScope(node)
2589
+ : context.getScope();
2590
+
2591
+ // Helper to find variable in scope chain
2592
+ const findVariableHandler = (s, varName) => {
2593
+ const v = s.variables.find((variable) => variable.name === varName);
2594
+
2595
+ if (v) return v;
2596
+ if (s.upper) return findVariableHandler(s.upper, varName);
2597
+
2598
+ return null;
2599
+ };
2600
+
2601
+ const variable = findVariableHandler(scope, localName);
2602
+
2603
+ if (variable) {
2604
+ const fixedRanges = new Set();
2605
+
2606
+ // Add the definition range to avoid double-fixing
2607
+ if (valueNode.range) {
2608
+ fixedRanges.add(`${valueNode.range[0]}-${valueNode.range[1]}`);
2609
+ }
2610
+
2611
+ // Fix all references
2612
+ variable.references.forEach((ref) => {
2613
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
2614
+
2615
+ if (!fixedRanges.has(rangeKey)) {
2616
+ fixedRanges.add(rangeKey);
2617
+ fixes.push(fixer.replaceText(ref.identifier, suggestedName));
2618
+ }
2619
+ });
2620
+ }
2621
+
2622
+ return fixes;
2623
+ },
2624
+ message: `Function "${localName}" destructured from hook should end with "Handler" suffix. Use "${suggestedName}" instead`,
2625
+ node: valueNode,
2626
+ });
2627
+ });
2628
+ };
2629
+
2486
2630
  return {
2487
2631
  ArrowFunctionExpression: checkFunctionHandler,
2488
2632
  FunctionDeclaration: checkFunctionHandler,
2489
2633
  FunctionExpression: checkFunctionHandler,
2490
2634
  MethodDefinition: checkMethodHandler,
2635
+ VariableDeclarator: checkDestructuredHookFunctionHandler,
2491
2636
  };
2492
2637
  },
2493
2638
  meta: {
@@ -3312,6 +3457,249 @@ const hookDepsPerLine = {
3312
3457
  },
3313
3458
  };
3314
3459
 
3460
+ /**
3461
+ * ───────────────────────────────────────────────────────────────
3462
+ * Rule: useState Naming Convention
3463
+ * ───────────────────────────────────────────────────────────────
3464
+ *
3465
+ * Description:
3466
+ * When useState holds a boolean value, the state variable name
3467
+ * should start with a valid boolean prefix (is, has, with, without).
3468
+ *
3469
+ * ✓ Good:
3470
+ * const [isLoading, setIsLoading] = useState(false);
3471
+ * const [hasError, setHasError] = useState(false);
3472
+ * const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => checkAuth());
3473
+ *
3474
+ * ✗ Bad:
3475
+ * const [loading, setLoading] = useState(false);
3476
+ * const [authenticated, setAuthenticated] = useState<boolean>(true);
3477
+ * const [error, setError] = useState<boolean>(false);
3478
+ */
3479
+ const useStateNamingConvention = {
3480
+ create(context) {
3481
+ const options = context.options[0] || {};
3482
+
3483
+ // Boolean prefixes handling (same pattern as prop-naming-convention)
3484
+ const defaultBooleanPrefixes = ["is", "has", "with", "without"];
3485
+ const booleanPrefixes = options.booleanPrefixes || [
3486
+ ...defaultBooleanPrefixes,
3487
+ ...(options.extendBooleanPrefixes || []),
3488
+ ];
3489
+
3490
+ const allowPastVerbBoolean = options.allowPastVerbBoolean || false;
3491
+ const allowContinuousVerbBoolean = options.allowContinuousVerbBoolean || false;
3492
+
3493
+ // Pattern to check if name starts with valid boolean prefix followed by capital letter
3494
+ const booleanPrefixPattern = new RegExp(`^(${booleanPrefixes.join("|")})[A-Z]`);
3495
+
3496
+ // Pattern for past verb booleans (ends with -ed: disabled, selected, checked, etc.)
3497
+ const pastVerbPattern = /^[a-z]+ed$/;
3498
+
3499
+ // Pattern for continuous verb booleans (ends with -ing: loading, saving, etc.)
3500
+ const continuousVerbPattern = /^[a-z]+ing$/;
3501
+
3502
+ // Words that suggest "has" prefix instead of "is"
3503
+ const hasKeywords = [
3504
+ "children", "content", "data", "error", "errors", "items",
3505
+ "permission", "permissions", "value", "values",
3506
+ ];
3507
+
3508
+ // Convert name to appropriate boolean prefix
3509
+ const toBooleanNameHandler = (name) => {
3510
+ const lowerName = name.toLowerCase();
3511
+ const prefix = hasKeywords.some((k) => lowerName.includes(k)) ? "has" : "is";
3512
+
3513
+ return prefix + name[0].toUpperCase() + name.slice(1);
3514
+ };
3515
+
3516
+ // Convert setter name based on new state name
3517
+ const toSetterNameHandler = (stateName) => "set" + stateName[0].toUpperCase() + stateName.slice(1);
3518
+
3519
+ // Check if name is a valid boolean state name
3520
+ const isValidBooleanNameHandler = (name) => {
3521
+ // Starts with valid prefix
3522
+ if (booleanPrefixPattern.test(name)) return true;
3523
+
3524
+ // Allow past verb booleans if option is enabled (disabled, selected, checked, etc.)
3525
+ if (allowPastVerbBoolean && pastVerbPattern.test(name)) return true;
3526
+
3527
+ // Allow continuous verb booleans if option is enabled (loading, saving, etc.)
3528
+ if (allowContinuousVerbBoolean && continuousVerbPattern.test(name)) return true;
3529
+
3530
+ return false;
3531
+ };
3532
+
3533
+ // Check if the useState initial value indicates boolean
3534
+ const isBooleanValueHandler = (arg) => {
3535
+ if (!arg) return false;
3536
+
3537
+ // Direct boolean literal: useState(false) or useState(true)
3538
+ if (arg.type === "Literal" && typeof arg.value === "boolean") return true;
3539
+
3540
+ // Arrow function returning boolean: useState(() => checkAuth())
3541
+ // We can't reliably determine return type, so skip these unless typed
3542
+ return false;
3543
+ };
3544
+
3545
+ // Check if the useState has boolean type annotation
3546
+ const hasBooleanTypeAnnotationHandler = (node) => {
3547
+ // useState<boolean>(...) or useState<boolean | null>(...)
3548
+ if (node.typeParameters && node.typeParameters.params && node.typeParameters.params.length > 0) {
3549
+ const typeParam = node.typeParameters.params[0];
3550
+
3551
+ if (typeParam.type === "TSBooleanKeyword") return true;
3552
+
3553
+ // Check union types: boolean | null, boolean | undefined
3554
+ if (typeParam.type === "TSUnionType") {
3555
+ return typeParam.types.some((t) => t.type === "TSBooleanKeyword");
3556
+ }
3557
+ }
3558
+
3559
+ return false;
3560
+ };
3561
+
3562
+ return {
3563
+ CallExpression(node) {
3564
+ // Check if it's a useState call
3565
+ if (node.callee.type !== "Identifier" || node.callee.name !== "useState") return;
3566
+
3567
+ // Check if it's in a variable declaration with array destructuring
3568
+ if (!node.parent || node.parent.type !== "VariableDeclarator") return;
3569
+ if (!node.parent.id || node.parent.id.type !== "ArrayPattern") return;
3570
+
3571
+ const arrayPattern = node.parent.id;
3572
+
3573
+ // Must have at least the state variable (first element)
3574
+ if (!arrayPattern.elements || arrayPattern.elements.length < 1) return;
3575
+
3576
+ const stateElement = arrayPattern.elements[0];
3577
+ const setterElement = arrayPattern.elements[1];
3578
+
3579
+ if (!stateElement || stateElement.type !== "Identifier") return;
3580
+
3581
+ const stateName = stateElement.name;
3582
+
3583
+ // Check if this is a boolean useState
3584
+ const isBooleanState = (node.arguments && node.arguments.length > 0 && isBooleanValueHandler(node.arguments[0]))
3585
+ || hasBooleanTypeAnnotationHandler(node);
3586
+
3587
+ if (!isBooleanState) return;
3588
+
3589
+ // Check if state name follows boolean naming convention
3590
+ if (isValidBooleanNameHandler(stateName)) return;
3591
+
3592
+ const suggestedStateName = toBooleanNameHandler(stateName);
3593
+ const suggestedSetterName = toSetterNameHandler(suggestedStateName);
3594
+
3595
+ context.report({
3596
+ fix(fixer) {
3597
+ const fixes = [];
3598
+ const scope = context.sourceCode
3599
+ ? context.sourceCode.getScope(node)
3600
+ : context.getScope();
3601
+
3602
+ // Helper to find variable in scope
3603
+ const findVariableHandler = (s, name) => {
3604
+ const v = s.variables.find((variable) => variable.name === name);
3605
+
3606
+ if (v) return v;
3607
+ if (s.upper) return findVariableHandler(s.upper, name);
3608
+
3609
+ return null;
3610
+ };
3611
+
3612
+ // Fix state variable
3613
+ const stateVar = findVariableHandler(scope, stateName);
3614
+ const stateFixedRanges = new Set();
3615
+
3616
+ // Fix definition first
3617
+ const stateDefRangeKey = `${stateElement.range[0]}-${stateElement.range[1]}`;
3618
+
3619
+ stateFixedRanges.add(stateDefRangeKey);
3620
+ fixes.push(fixer.replaceText(stateElement, suggestedStateName));
3621
+
3622
+ // Fix all usages
3623
+ if (stateVar) {
3624
+ stateVar.references.forEach((ref) => {
3625
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
3626
+
3627
+ if (!stateFixedRanges.has(rangeKey)) {
3628
+ stateFixedRanges.add(rangeKey);
3629
+ fixes.push(fixer.replaceText(ref.identifier, suggestedStateName));
3630
+ }
3631
+ });
3632
+ }
3633
+
3634
+ // Fix setter if exists
3635
+ if (setterElement && setterElement.type === "Identifier") {
3636
+ const setterName = setterElement.name;
3637
+ const setterVar = findVariableHandler(scope, setterName);
3638
+ const setterFixedRanges = new Set();
3639
+
3640
+ // Fix definition first
3641
+ const setterDefRangeKey = `${setterElement.range[0]}-${setterElement.range[1]}`;
3642
+
3643
+ setterFixedRanges.add(setterDefRangeKey);
3644
+ fixes.push(fixer.replaceText(setterElement, suggestedSetterName));
3645
+
3646
+ // Fix all usages
3647
+ if (setterVar) {
3648
+ setterVar.references.forEach((ref) => {
3649
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
3650
+
3651
+ if (!setterFixedRanges.has(rangeKey)) {
3652
+ setterFixedRanges.add(rangeKey);
3653
+ fixes.push(fixer.replaceText(ref.identifier, suggestedSetterName));
3654
+ }
3655
+ });
3656
+ }
3657
+ }
3658
+
3659
+ return fixes;
3660
+ },
3661
+ message: `Boolean state "${stateName}" should start with a valid prefix (${booleanPrefixes.join(", ")}). Use "${suggestedStateName}" instead.`,
3662
+ node: stateElement,
3663
+ });
3664
+ },
3665
+ };
3666
+ },
3667
+ meta: {
3668
+ docs: { description: "Enforce boolean useState variables to start with is/has/with/without prefix" },
3669
+ fixable: "code",
3670
+ schema: [
3671
+ {
3672
+ additionalProperties: false,
3673
+ properties: {
3674
+ allowContinuousVerbBoolean: {
3675
+ default: false,
3676
+ description: "Allow continuous verb boolean state without prefix (e.g., loading, saving)",
3677
+ type: "boolean",
3678
+ },
3679
+ allowPastVerbBoolean: {
3680
+ default: false,
3681
+ description: "Allow past verb boolean state without prefix (e.g., disabled, selected)",
3682
+ type: "boolean",
3683
+ },
3684
+ booleanPrefixes: {
3685
+ description: "Replace default boolean prefixes entirely",
3686
+ items: { type: "string" },
3687
+ type: "array",
3688
+ },
3689
+ extendBooleanPrefixes: {
3690
+ default: [],
3691
+ description: "Add additional prefixes to the defaults (is, has, with, without)",
3692
+ items: { type: "string" },
3693
+ type: "array",
3694
+ },
3695
+ },
3696
+ type: "object",
3697
+ },
3698
+ ],
3699
+ type: "suggestion",
3700
+ },
3701
+ };
3702
+
3315
3703
  /**
3316
3704
  * ───────────────────────────────────────────────────────────────
3317
3705
  * Rule: If Statement Format
@@ -8775,6 +9163,111 @@ const propNamingConvention = {
8775
9163
  return false;
8776
9164
  };
8777
9165
 
9166
+ // Find the containing function for inline type annotations
9167
+ const findContainingFunctionHandler = (node) => {
9168
+ let current = node;
9169
+
9170
+ while (current) {
9171
+ if (current.type === "ArrowFunctionExpression" ||
9172
+ current.type === "FunctionExpression" ||
9173
+ current.type === "FunctionDeclaration") {
9174
+ return current;
9175
+ }
9176
+
9177
+ current = current.parent;
9178
+ }
9179
+
9180
+ return null;
9181
+ };
9182
+
9183
+ // Find matching property in ObjectPattern (destructured params)
9184
+ const findDestructuredPropertyHandler = (funcNode, propName) => {
9185
+ if (!funcNode || !funcNode.params || funcNode.params.length === 0) return null;
9186
+
9187
+ const firstParam = funcNode.params[0];
9188
+
9189
+ if (firstParam.type !== "ObjectPattern") return null;
9190
+
9191
+ // Find the property in the ObjectPattern
9192
+ for (const prop of firstParam.properties) {
9193
+ if (prop.type === "Property" && prop.key && prop.key.type === "Identifier") {
9194
+ if (prop.key.name === propName) {
9195
+ return prop;
9196
+ }
9197
+ }
9198
+ }
9199
+
9200
+ return null;
9201
+ };
9202
+
9203
+ // Create fix that renames type annotation, destructured prop, and all references
9204
+ const createRenamingFixHandler = (fixer, member, propName, suggestedName) => {
9205
+ const fixes = [fixer.replaceText(member.key, suggestedName)];
9206
+
9207
+ // Find the containing function
9208
+ const funcNode = findContainingFunctionHandler(member);
9209
+
9210
+ if (!funcNode) return fixes;
9211
+
9212
+ // Find the matching destructured property
9213
+ const destructuredProp = findDestructuredPropertyHandler(funcNode, propName);
9214
+
9215
+ if (!destructuredProp) return fixes;
9216
+
9217
+ // Get the value node (the actual variable being declared)
9218
+ const valueNode = destructuredProp.value || destructuredProp.key;
9219
+
9220
+ if (!valueNode || valueNode.type !== "Identifier") return fixes;
9221
+
9222
+ // If key and value are the same (shorthand: { copied }), we need to expand and rename
9223
+ // If different (renamed: { copied: isCopied }), just rename the value
9224
+ const isShorthand = destructuredProp.shorthand !== false &&
9225
+ destructuredProp.key === destructuredProp.value;
9226
+
9227
+ if (isShorthand) {
9228
+ // Shorthand syntax: { copied } -> { copied: isCopied }
9229
+ // We need to keep the key (for the type match) and add the renamed value
9230
+ fixes.push(fixer.replaceText(destructuredProp, `${propName}: ${suggestedName}`));
9231
+ } else {
9232
+ // Already renamed syntax or explicit: just update the value
9233
+ fixes.push(fixer.replaceText(valueNode, suggestedName));
9234
+ }
9235
+
9236
+ // Find all references to the variable and rename them
9237
+ const scope = context.sourceCode
9238
+ ? context.sourceCode.getScope(funcNode)
9239
+ : context.getScope();
9240
+
9241
+ // Find the variable in the scope
9242
+ const findVariableHandler = (s, name) => {
9243
+ const v = s.variables.find((variable) => variable.name === name);
9244
+
9245
+ if (v) return v;
9246
+ if (s.upper) return findVariableHandler(s.upper, name);
9247
+
9248
+ return null;
9249
+ };
9250
+
9251
+ const variable = findVariableHandler(scope, propName);
9252
+
9253
+ if (variable) {
9254
+ const fixedRanges = new Set();
9255
+
9256
+ variable.references.forEach((ref) => {
9257
+ // Skip the definition itself
9258
+ if (ref.identifier === valueNode) return;
9259
+ // Skip if already fixed
9260
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
9261
+
9262
+ if (fixedRanges.has(rangeKey)) return;
9263
+ fixedRanges.add(rangeKey);
9264
+ fixes.push(fixer.replaceText(ref.identifier, suggestedName));
9265
+ });
9266
+ }
9267
+
9268
+ return fixes;
9269
+ };
9270
+
8778
9271
  // Check a property signature (interface/type member) - recursive for nested types
8779
9272
  const checkPropertySignatureHandler = (member) => {
8780
9273
  if (member.type !== "TSPropertySignature") return;
@@ -8802,7 +9295,7 @@ const propNamingConvention = {
8802
9295
  const suggestedName = toBooleanNameHandler(propName);
8803
9296
 
8804
9297
  context.report({
8805
- fix: (fixer) => fixer.replaceText(member.key, suggestedName),
9298
+ fix: (fixer) => createRenamingFixHandler(fixer, member, propName, suggestedName),
8806
9299
  message: `Boolean prop "${propName}" should start with a valid prefix (${booleanPrefixes.join(", ")}). Use "${suggestedName}" instead.`,
8807
9300
  node: member.key,
8808
9301
  });
@@ -8817,7 +9310,7 @@ const propNamingConvention = {
8817
9310
  const suggestedName = toCallbackNameHandler(propName);
8818
9311
 
8819
9312
  context.report({
8820
- fix: (fixer) => fixer.replaceText(member.key, suggestedName),
9313
+ fix: (fixer) => createRenamingFixHandler(fixer, member, propName, suggestedName),
8821
9314
  message: `Callback prop "${propName}" should start with "${callbackPrefix}" prefix. Use "${suggestedName}" instead.`,
8822
9315
  node: member.key,
8823
9316
  });
@@ -18487,8 +18980,54 @@ const folderComponentSuffix = {
18487
18980
 
18488
18981
  // Check if component name ends with the required suffix
18489
18982
  if (!name.endsWith(suffix)) {
18983
+ const newName = `${name}${suffix}`;
18984
+
18490
18985
  context.report({
18491
- message: `Component "${name}" in "${folder}" folder must end with "${suffix}" suffix (e.g., "${name}${suffix}")`,
18986
+ fix(fixer) {
18987
+ const scope = context.sourceCode
18988
+ ? context.sourceCode.getScope(node)
18989
+ : context.getScope();
18990
+
18991
+ // Find the variable in scope
18992
+ const findVariableHandler = (s, varName) => {
18993
+ const v = s.variables.find((variable) => variable.name === varName);
18994
+
18995
+ if (v) return v;
18996
+ if (s.upper) return findVariableHandler(s.upper, varName);
18997
+
18998
+ return null;
18999
+ };
19000
+
19001
+ const variable = findVariableHandler(scope, name);
19002
+
19003
+ if (!variable) return fixer.replaceText(identifierNode, newName);
19004
+
19005
+ const fixes = [];
19006
+ const fixedRanges = new Set();
19007
+
19008
+ // Fix definition
19009
+ variable.defs.forEach((def) => {
19010
+ const rangeKey = `${def.name.range[0]}-${def.name.range[1]}`;
19011
+
19012
+ if (!fixedRanges.has(rangeKey)) {
19013
+ fixedRanges.add(rangeKey);
19014
+ fixes.push(fixer.replaceText(def.name, newName));
19015
+ }
19016
+ });
19017
+
19018
+ // Fix all references
19019
+ variable.references.forEach((ref) => {
19020
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
19021
+
19022
+ if (!fixedRanges.has(rangeKey)) {
19023
+ fixedRanges.add(rangeKey);
19024
+ fixes.push(fixer.replaceText(ref.identifier, newName));
19025
+ }
19026
+ });
19027
+
19028
+ return fixes;
19029
+ },
19030
+ message: `Component "${name}" in "${folder}" folder must end with "${suffix}" suffix (e.g., "${newName}")`,
18492
19031
  node: identifierNode,
18493
19032
  });
18494
19033
  }
@@ -18502,7 +19041,7 @@ const folderComponentSuffix = {
18502
19041
  },
18503
19042
  meta: {
18504
19043
  docs: { description: "Enforce components in 'views' folder end with 'View' and components in 'pages' folder end with 'Page'" },
18505
- fixable: null,
19044
+ fixable: "code",
18506
19045
  schema: [],
18507
19046
  type: "suggestion",
18508
19047
  },
@@ -21694,6 +22233,7 @@ export default {
21694
22233
  // Hook rules
21695
22234
  "hook-callback-format": hookCallbackFormat,
21696
22235
  "hook-deps-per-line": hookDepsPerLine,
22236
+ "use-state-naming-convention": useStateNamingConvention,
21697
22237
 
21698
22238
  // Import/Export rules
21699
22239
  "absolute-imports-only": absoluteImportsOnly,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "A custom ESLint plugin for enforcing consistent code formatting and style rules in React/JSX projects",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",