eslint-plugin-code-style 1.13.0 → 1.14.0

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,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.14.0] - 2026-02-05
11
+
12
+ **New Rule: useState Naming Convention**
13
+
14
+ **Version Range:** v1.13.0 → v1.14.0
15
+
16
+ ### Added
17
+
18
+ **New Rules (1)**
19
+ - `use-state-naming-convention` - Enforce boolean useState variables to start with valid prefixes 🔧
20
+ - Boolean state must start with: `is`, `has`, `with`, `without` (configurable)
21
+ - Auto-fixes both state variable and setter function names, plus all usages
22
+ - Detects boolean literals (`useState(false)`) and type annotations (`useState<boolean>()`)
23
+ - Options: `booleanPrefixes`, `extendBooleanPrefixes`, `allowPastVerbBoolean`, `allowContinuousVerbBoolean`
24
+
25
+ ### Enhanced
26
+
27
+ - **`folder-component-suffix`** - Add auto-fix to rename component and all references in the file
28
+ - **`function-naming-convention`** - Detect useCallback-wrapped functions in custom hooks
29
+ - **`prop-naming-convention`** - Auto-fix now renames both type annotation AND destructured parameter with all usages
30
+
31
+ ### Stats
32
+
33
+ - Total Rules: 76 (was 75)
34
+ - Auto-fixable: 67 rules 🔧
35
+ - Configurable: 17 rules ⚙️
36
+ - Report-only: 9 rules
37
+
38
+ **Full Changelog:** [v1.13.0...v1.14.0](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.13.0...v1.14.0)
39
+
40
+ ---
41
+
10
42
  ## [1.13.0] - 2026-02-05
11
43
 
12
44
  **New Rule: Prop Naming Convention & Auto-Fix Enhancements**
@@ -1622,6 +1654,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1622
1654
 
1623
1655
  ---
1624
1656
 
1657
+ [1.14.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.13.0...v1.14.0
1625
1658
  [1.13.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.12.1...v1.13.0
1626
1659
  [1.12.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.12.0...v1.12.1
1627
1660
  [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({
@@ -3312,6 +3329,249 @@ const hookDepsPerLine = {
3312
3329
  },
3313
3330
  };
3314
3331
 
3332
+ /**
3333
+ * ───────────────────────────────────────────────────────────────
3334
+ * Rule: useState Naming Convention
3335
+ * ───────────────────────────────────────────────────────────────
3336
+ *
3337
+ * Description:
3338
+ * When useState holds a boolean value, the state variable name
3339
+ * should start with a valid boolean prefix (is, has, with, without).
3340
+ *
3341
+ * ✓ Good:
3342
+ * const [isLoading, setIsLoading] = useState(false);
3343
+ * const [hasError, setHasError] = useState(false);
3344
+ * const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => checkAuth());
3345
+ *
3346
+ * ✗ Bad:
3347
+ * const [loading, setLoading] = useState(false);
3348
+ * const [authenticated, setAuthenticated] = useState<boolean>(true);
3349
+ * const [error, setError] = useState<boolean>(false);
3350
+ */
3351
+ const useStateNamingConvention = {
3352
+ create(context) {
3353
+ const options = context.options[0] || {};
3354
+
3355
+ // Boolean prefixes handling (same pattern as prop-naming-convention)
3356
+ const defaultBooleanPrefixes = ["is", "has", "with", "without"];
3357
+ const booleanPrefixes = options.booleanPrefixes || [
3358
+ ...defaultBooleanPrefixes,
3359
+ ...(options.extendBooleanPrefixes || []),
3360
+ ];
3361
+
3362
+ const allowPastVerbBoolean = options.allowPastVerbBoolean || false;
3363
+ const allowContinuousVerbBoolean = options.allowContinuousVerbBoolean || false;
3364
+
3365
+ // Pattern to check if name starts with valid boolean prefix followed by capital letter
3366
+ const booleanPrefixPattern = new RegExp(`^(${booleanPrefixes.join("|")})[A-Z]`);
3367
+
3368
+ // Pattern for past verb booleans (ends with -ed: disabled, selected, checked, etc.)
3369
+ const pastVerbPattern = /^[a-z]+ed$/;
3370
+
3371
+ // Pattern for continuous verb booleans (ends with -ing: loading, saving, etc.)
3372
+ const continuousVerbPattern = /^[a-z]+ing$/;
3373
+
3374
+ // Words that suggest "has" prefix instead of "is"
3375
+ const hasKeywords = [
3376
+ "children", "content", "data", "error", "errors", "items",
3377
+ "permission", "permissions", "value", "values",
3378
+ ];
3379
+
3380
+ // Convert name to appropriate boolean prefix
3381
+ const toBooleanNameHandler = (name) => {
3382
+ const lowerName = name.toLowerCase();
3383
+ const prefix = hasKeywords.some((k) => lowerName.includes(k)) ? "has" : "is";
3384
+
3385
+ return prefix + name[0].toUpperCase() + name.slice(1);
3386
+ };
3387
+
3388
+ // Convert setter name based on new state name
3389
+ const toSetterNameHandler = (stateName) => "set" + stateName[0].toUpperCase() + stateName.slice(1);
3390
+
3391
+ // Check if name is a valid boolean state name
3392
+ const isValidBooleanNameHandler = (name) => {
3393
+ // Starts with valid prefix
3394
+ if (booleanPrefixPattern.test(name)) return true;
3395
+
3396
+ // Allow past verb booleans if option is enabled (disabled, selected, checked, etc.)
3397
+ if (allowPastVerbBoolean && pastVerbPattern.test(name)) return true;
3398
+
3399
+ // Allow continuous verb booleans if option is enabled (loading, saving, etc.)
3400
+ if (allowContinuousVerbBoolean && continuousVerbPattern.test(name)) return true;
3401
+
3402
+ return false;
3403
+ };
3404
+
3405
+ // Check if the useState initial value indicates boolean
3406
+ const isBooleanValueHandler = (arg) => {
3407
+ if (!arg) return false;
3408
+
3409
+ // Direct boolean literal: useState(false) or useState(true)
3410
+ if (arg.type === "Literal" && typeof arg.value === "boolean") return true;
3411
+
3412
+ // Arrow function returning boolean: useState(() => checkAuth())
3413
+ // We can't reliably determine return type, so skip these unless typed
3414
+ return false;
3415
+ };
3416
+
3417
+ // Check if the useState has boolean type annotation
3418
+ const hasBooleanTypeAnnotationHandler = (node) => {
3419
+ // useState<boolean>(...) or useState<boolean | null>(...)
3420
+ if (node.typeParameters && node.typeParameters.params && node.typeParameters.params.length > 0) {
3421
+ const typeParam = node.typeParameters.params[0];
3422
+
3423
+ if (typeParam.type === "TSBooleanKeyword") return true;
3424
+
3425
+ // Check union types: boolean | null, boolean | undefined
3426
+ if (typeParam.type === "TSUnionType") {
3427
+ return typeParam.types.some((t) => t.type === "TSBooleanKeyword");
3428
+ }
3429
+ }
3430
+
3431
+ return false;
3432
+ };
3433
+
3434
+ return {
3435
+ CallExpression(node) {
3436
+ // Check if it's a useState call
3437
+ if (node.callee.type !== "Identifier" || node.callee.name !== "useState") return;
3438
+
3439
+ // Check if it's in a variable declaration with array destructuring
3440
+ if (!node.parent || node.parent.type !== "VariableDeclarator") return;
3441
+ if (!node.parent.id || node.parent.id.type !== "ArrayPattern") return;
3442
+
3443
+ const arrayPattern = node.parent.id;
3444
+
3445
+ // Must have at least the state variable (first element)
3446
+ if (!arrayPattern.elements || arrayPattern.elements.length < 1) return;
3447
+
3448
+ const stateElement = arrayPattern.elements[0];
3449
+ const setterElement = arrayPattern.elements[1];
3450
+
3451
+ if (!stateElement || stateElement.type !== "Identifier") return;
3452
+
3453
+ const stateName = stateElement.name;
3454
+
3455
+ // Check if this is a boolean useState
3456
+ const isBooleanState = (node.arguments && node.arguments.length > 0 && isBooleanValueHandler(node.arguments[0]))
3457
+ || hasBooleanTypeAnnotationHandler(node);
3458
+
3459
+ if (!isBooleanState) return;
3460
+
3461
+ // Check if state name follows boolean naming convention
3462
+ if (isValidBooleanNameHandler(stateName)) return;
3463
+
3464
+ const suggestedStateName = toBooleanNameHandler(stateName);
3465
+ const suggestedSetterName = toSetterNameHandler(suggestedStateName);
3466
+
3467
+ context.report({
3468
+ fix(fixer) {
3469
+ const fixes = [];
3470
+ const scope = context.sourceCode
3471
+ ? context.sourceCode.getScope(node)
3472
+ : context.getScope();
3473
+
3474
+ // Helper to find variable in scope
3475
+ const findVariableHandler = (s, name) => {
3476
+ const v = s.variables.find((variable) => variable.name === name);
3477
+
3478
+ if (v) return v;
3479
+ if (s.upper) return findVariableHandler(s.upper, name);
3480
+
3481
+ return null;
3482
+ };
3483
+
3484
+ // Fix state variable
3485
+ const stateVar = findVariableHandler(scope, stateName);
3486
+ const stateFixedRanges = new Set();
3487
+
3488
+ // Fix definition first
3489
+ const stateDefRangeKey = `${stateElement.range[0]}-${stateElement.range[1]}`;
3490
+
3491
+ stateFixedRanges.add(stateDefRangeKey);
3492
+ fixes.push(fixer.replaceText(stateElement, suggestedStateName));
3493
+
3494
+ // Fix all usages
3495
+ if (stateVar) {
3496
+ stateVar.references.forEach((ref) => {
3497
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
3498
+
3499
+ if (!stateFixedRanges.has(rangeKey)) {
3500
+ stateFixedRanges.add(rangeKey);
3501
+ fixes.push(fixer.replaceText(ref.identifier, suggestedStateName));
3502
+ }
3503
+ });
3504
+ }
3505
+
3506
+ // Fix setter if exists
3507
+ if (setterElement && setterElement.type === "Identifier") {
3508
+ const setterName = setterElement.name;
3509
+ const setterVar = findVariableHandler(scope, setterName);
3510
+ const setterFixedRanges = new Set();
3511
+
3512
+ // Fix definition first
3513
+ const setterDefRangeKey = `${setterElement.range[0]}-${setterElement.range[1]}`;
3514
+
3515
+ setterFixedRanges.add(setterDefRangeKey);
3516
+ fixes.push(fixer.replaceText(setterElement, suggestedSetterName));
3517
+
3518
+ // Fix all usages
3519
+ if (setterVar) {
3520
+ setterVar.references.forEach((ref) => {
3521
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
3522
+
3523
+ if (!setterFixedRanges.has(rangeKey)) {
3524
+ setterFixedRanges.add(rangeKey);
3525
+ fixes.push(fixer.replaceText(ref.identifier, suggestedSetterName));
3526
+ }
3527
+ });
3528
+ }
3529
+ }
3530
+
3531
+ return fixes;
3532
+ },
3533
+ message: `Boolean state "${stateName}" should start with a valid prefix (${booleanPrefixes.join(", ")}). Use "${suggestedStateName}" instead.`,
3534
+ node: stateElement,
3535
+ });
3536
+ },
3537
+ };
3538
+ },
3539
+ meta: {
3540
+ docs: { description: "Enforce boolean useState variables to start with is/has/with/without prefix" },
3541
+ fixable: "code",
3542
+ schema: [
3543
+ {
3544
+ additionalProperties: false,
3545
+ properties: {
3546
+ allowContinuousVerbBoolean: {
3547
+ default: false,
3548
+ description: "Allow continuous verb boolean state without prefix (e.g., loading, saving)",
3549
+ type: "boolean",
3550
+ },
3551
+ allowPastVerbBoolean: {
3552
+ default: false,
3553
+ description: "Allow past verb boolean state without prefix (e.g., disabled, selected)",
3554
+ type: "boolean",
3555
+ },
3556
+ booleanPrefixes: {
3557
+ description: "Replace default boolean prefixes entirely",
3558
+ items: { type: "string" },
3559
+ type: "array",
3560
+ },
3561
+ extendBooleanPrefixes: {
3562
+ default: [],
3563
+ description: "Add additional prefixes to the defaults (is, has, with, without)",
3564
+ items: { type: "string" },
3565
+ type: "array",
3566
+ },
3567
+ },
3568
+ type: "object",
3569
+ },
3570
+ ],
3571
+ type: "suggestion",
3572
+ },
3573
+ };
3574
+
3315
3575
  /**
3316
3576
  * ───────────────────────────────────────────────────────────────
3317
3577
  * Rule: If Statement Format
@@ -8775,6 +9035,111 @@ const propNamingConvention = {
8775
9035
  return false;
8776
9036
  };
8777
9037
 
9038
+ // Find the containing function for inline type annotations
9039
+ const findContainingFunctionHandler = (node) => {
9040
+ let current = node;
9041
+
9042
+ while (current) {
9043
+ if (current.type === "ArrowFunctionExpression" ||
9044
+ current.type === "FunctionExpression" ||
9045
+ current.type === "FunctionDeclaration") {
9046
+ return current;
9047
+ }
9048
+
9049
+ current = current.parent;
9050
+ }
9051
+
9052
+ return null;
9053
+ };
9054
+
9055
+ // Find matching property in ObjectPattern (destructured params)
9056
+ const findDestructuredPropertyHandler = (funcNode, propName) => {
9057
+ if (!funcNode || !funcNode.params || funcNode.params.length === 0) return null;
9058
+
9059
+ const firstParam = funcNode.params[0];
9060
+
9061
+ if (firstParam.type !== "ObjectPattern") return null;
9062
+
9063
+ // Find the property in the ObjectPattern
9064
+ for (const prop of firstParam.properties) {
9065
+ if (prop.type === "Property" && prop.key && prop.key.type === "Identifier") {
9066
+ if (prop.key.name === propName) {
9067
+ return prop;
9068
+ }
9069
+ }
9070
+ }
9071
+
9072
+ return null;
9073
+ };
9074
+
9075
+ // Create fix that renames type annotation, destructured prop, and all references
9076
+ const createRenamingFixHandler = (fixer, member, propName, suggestedName) => {
9077
+ const fixes = [fixer.replaceText(member.key, suggestedName)];
9078
+
9079
+ // Find the containing function
9080
+ const funcNode = findContainingFunctionHandler(member);
9081
+
9082
+ if (!funcNode) return fixes;
9083
+
9084
+ // Find the matching destructured property
9085
+ const destructuredProp = findDestructuredPropertyHandler(funcNode, propName);
9086
+
9087
+ if (!destructuredProp) return fixes;
9088
+
9089
+ // Get the value node (the actual variable being declared)
9090
+ const valueNode = destructuredProp.value || destructuredProp.key;
9091
+
9092
+ if (!valueNode || valueNode.type !== "Identifier") return fixes;
9093
+
9094
+ // If key and value are the same (shorthand: { copied }), we need to expand and rename
9095
+ // If different (renamed: { copied: isCopied }), just rename the value
9096
+ const isShorthand = destructuredProp.shorthand !== false &&
9097
+ destructuredProp.key === destructuredProp.value;
9098
+
9099
+ if (isShorthand) {
9100
+ // Shorthand syntax: { copied } -> { copied: isCopied }
9101
+ // We need to keep the key (for the type match) and add the renamed value
9102
+ fixes.push(fixer.replaceText(destructuredProp, `${propName}: ${suggestedName}`));
9103
+ } else {
9104
+ // Already renamed syntax or explicit: just update the value
9105
+ fixes.push(fixer.replaceText(valueNode, suggestedName));
9106
+ }
9107
+
9108
+ // Find all references to the variable and rename them
9109
+ const scope = context.sourceCode
9110
+ ? context.sourceCode.getScope(funcNode)
9111
+ : context.getScope();
9112
+
9113
+ // Find the variable in the scope
9114
+ const findVariableHandler = (s, name) => {
9115
+ const v = s.variables.find((variable) => variable.name === name);
9116
+
9117
+ if (v) return v;
9118
+ if (s.upper) return findVariableHandler(s.upper, name);
9119
+
9120
+ return null;
9121
+ };
9122
+
9123
+ const variable = findVariableHandler(scope, propName);
9124
+
9125
+ if (variable) {
9126
+ const fixedRanges = new Set();
9127
+
9128
+ variable.references.forEach((ref) => {
9129
+ // Skip the definition itself
9130
+ if (ref.identifier === valueNode) return;
9131
+ // Skip if already fixed
9132
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
9133
+
9134
+ if (fixedRanges.has(rangeKey)) return;
9135
+ fixedRanges.add(rangeKey);
9136
+ fixes.push(fixer.replaceText(ref.identifier, suggestedName));
9137
+ });
9138
+ }
9139
+
9140
+ return fixes;
9141
+ };
9142
+
8778
9143
  // Check a property signature (interface/type member) - recursive for nested types
8779
9144
  const checkPropertySignatureHandler = (member) => {
8780
9145
  if (member.type !== "TSPropertySignature") return;
@@ -8802,7 +9167,7 @@ const propNamingConvention = {
8802
9167
  const suggestedName = toBooleanNameHandler(propName);
8803
9168
 
8804
9169
  context.report({
8805
- fix: (fixer) => fixer.replaceText(member.key, suggestedName),
9170
+ fix: (fixer) => createRenamingFixHandler(fixer, member, propName, suggestedName),
8806
9171
  message: `Boolean prop "${propName}" should start with a valid prefix (${booleanPrefixes.join(", ")}). Use "${suggestedName}" instead.`,
8807
9172
  node: member.key,
8808
9173
  });
@@ -8817,7 +9182,7 @@ const propNamingConvention = {
8817
9182
  const suggestedName = toCallbackNameHandler(propName);
8818
9183
 
8819
9184
  context.report({
8820
- fix: (fixer) => fixer.replaceText(member.key, suggestedName),
9185
+ fix: (fixer) => createRenamingFixHandler(fixer, member, propName, suggestedName),
8821
9186
  message: `Callback prop "${propName}" should start with "${callbackPrefix}" prefix. Use "${suggestedName}" instead.`,
8822
9187
  node: member.key,
8823
9188
  });
@@ -18487,8 +18852,54 @@ const folderComponentSuffix = {
18487
18852
 
18488
18853
  // Check if component name ends with the required suffix
18489
18854
  if (!name.endsWith(suffix)) {
18855
+ const newName = `${name}${suffix}`;
18856
+
18490
18857
  context.report({
18491
- message: `Component "${name}" in "${folder}" folder must end with "${suffix}" suffix (e.g., "${name}${suffix}")`,
18858
+ fix(fixer) {
18859
+ const scope = context.sourceCode
18860
+ ? context.sourceCode.getScope(node)
18861
+ : context.getScope();
18862
+
18863
+ // Find the variable in scope
18864
+ const findVariableHandler = (s, varName) => {
18865
+ const v = s.variables.find((variable) => variable.name === varName);
18866
+
18867
+ if (v) return v;
18868
+ if (s.upper) return findVariableHandler(s.upper, varName);
18869
+
18870
+ return null;
18871
+ };
18872
+
18873
+ const variable = findVariableHandler(scope, name);
18874
+
18875
+ if (!variable) return fixer.replaceText(identifierNode, newName);
18876
+
18877
+ const fixes = [];
18878
+ const fixedRanges = new Set();
18879
+
18880
+ // Fix definition
18881
+ variable.defs.forEach((def) => {
18882
+ const rangeKey = `${def.name.range[0]}-${def.name.range[1]}`;
18883
+
18884
+ if (!fixedRanges.has(rangeKey)) {
18885
+ fixedRanges.add(rangeKey);
18886
+ fixes.push(fixer.replaceText(def.name, newName));
18887
+ }
18888
+ });
18889
+
18890
+ // Fix all references
18891
+ variable.references.forEach((ref) => {
18892
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
18893
+
18894
+ if (!fixedRanges.has(rangeKey)) {
18895
+ fixedRanges.add(rangeKey);
18896
+ fixes.push(fixer.replaceText(ref.identifier, newName));
18897
+ }
18898
+ });
18899
+
18900
+ return fixes;
18901
+ },
18902
+ message: `Component "${name}" in "${folder}" folder must end with "${suffix}" suffix (e.g., "${newName}")`,
18492
18903
  node: identifierNode,
18493
18904
  });
18494
18905
  }
@@ -18502,7 +18913,7 @@ const folderComponentSuffix = {
18502
18913
  },
18503
18914
  meta: {
18504
18915
  docs: { description: "Enforce components in 'views' folder end with 'View' and components in 'pages' folder end with 'Page'" },
18505
- fixable: null,
18916
+ fixable: "code",
18506
18917
  schema: [],
18507
18918
  type: "suggestion",
18508
18919
  },
@@ -21694,6 +22105,7 @@ export default {
21694
22105
  // Hook rules
21695
22106
  "hook-callback-format": hookCallbackFormat,
21696
22107
  "hook-deps-per-line": hookDepsPerLine,
22108
+ "use-state-naming-convention": useStateNamingConvention,
21697
22109
 
21698
22110
  // Import/Export rules
21699
22111
  "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.0",
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",