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 +33 -0
- package/README.md +50 -5
- package/index.d.ts +1 -0
- package/index.js +419 -7
- package/package.json +1 -1
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
|
-
*
|
|
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** —
|
|
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
|
-
**
|
|
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
|
-
> **
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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