eslint-plugin-code-style 1.8.4 → 1.9.2
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 +69 -2
- package/README.md +90 -12
- package/index.d.ts +2 -0
- package/index.js +561 -89
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.9.2] - 2026-02-03
|
|
11
|
+
|
|
12
|
+
### Enhanced
|
|
13
|
+
|
|
14
|
+
- **`no-hardcoded-strings`** - Now detects special strings that should be enums:
|
|
15
|
+
- HTTP status codes (2xx, 4xx, 5xx like "200", "404", "500")
|
|
16
|
+
- HTTP methods ("GET", "POST", "PUT", "DELETE", etc.)
|
|
17
|
+
- Role/permission names ("admin", "user", "moderator", etc.)
|
|
18
|
+
- Environment names ("production", "development", "staging", etc.)
|
|
19
|
+
- Log levels ("debug", "info", "warn", "error", etc.)
|
|
20
|
+
- Status strings ("active", "pending", "approved", "rejected", etc.)
|
|
21
|
+
- Priority levels ("high", "medium", "low", "critical", etc.)
|
|
22
|
+
- All above → import from `@/enums` or `@/data`
|
|
23
|
+
- Regular strings → import from `@/data or @/strings or @/constants or @/@constants or @/@strings`
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **`no-hardcoded-strings`** - Fixed bug where `isInConstantsObjectHandler` incorrectly matched camelCase variable names due to case-insensitive regex flag
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## [1.9.1] - 2026-02-03
|
|
32
|
+
|
|
33
|
+
### Enhanced
|
|
34
|
+
|
|
35
|
+
- **`no-hardcoded-strings`** - Initial special string detection:
|
|
36
|
+
- HTTP status codes (4xx, 5xx)
|
|
37
|
+
- Role/permission names
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## [1.9.0] - 2026-02-03
|
|
42
|
+
|
|
43
|
+
**New Rule: class-method-definition-format + Enhanced Spacing Rules**
|
|
44
|
+
|
|
45
|
+
**Version Range:** v1.8.1 → v1.9.0
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
**New Rules (1)**
|
|
50
|
+
- **`class-method-definition-format`** - Enforce consistent spacing in class and method definitions 🔧
|
|
51
|
+
- Space before opening brace `{` in class declarations
|
|
52
|
+
- No space between method name and opening parenthesis `(`
|
|
53
|
+
- Space before opening brace `{` in method definitions
|
|
54
|
+
- Opening brace must be on same line as class/method signature
|
|
55
|
+
|
|
56
|
+
### Enhanced
|
|
57
|
+
|
|
58
|
+
- **`function-call-spacing`** - Now also handles:
|
|
59
|
+
- `new` expressions (`new Class ()` → `new Class()`)
|
|
60
|
+
- Generic type calls (`get <Type>()` → `get<Type>()`)
|
|
61
|
+
- **`member-expression-bracket-spacing`** - Now also handles:
|
|
62
|
+
- TypeScript indexed access types (`Type ["prop"]` → `Type["prop"]`)
|
|
63
|
+
|
|
64
|
+
### Stats
|
|
65
|
+
|
|
66
|
+
- Total Rules: 71 (was 70)
|
|
67
|
+
- Auto-fixable: 64 rules 🔧 (was 63)
|
|
68
|
+
- Report-only: 7 rules
|
|
69
|
+
|
|
70
|
+
**Full Changelog:** [v1.8.1...v1.9.0](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.8.1...v1.9.0)
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
10
74
|
## [1.8.4] - 2026-02-03
|
|
11
75
|
|
|
12
76
|
### Documentation
|
|
@@ -54,7 +118,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
54
118
|
|
|
55
119
|
**New Rule: no-hardcoded-strings**
|
|
56
120
|
|
|
57
|
-
**Version Range:** v1.
|
|
121
|
+
**Version Range:** v1.7.1 → v1.8.0
|
|
58
122
|
|
|
59
123
|
### Added
|
|
60
124
|
|
|
@@ -76,7 +140,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
76
140
|
- Auto-fixable: 63 rules 🔧
|
|
77
141
|
- Report-only: 7 rules (was 6)
|
|
78
142
|
|
|
79
|
-
**Full Changelog:** [v1.
|
|
143
|
+
**Full Changelog:** [v1.7.1...v1.8.0](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.1...v1.8.0)
|
|
80
144
|
|
|
81
145
|
---
|
|
82
146
|
|
|
@@ -1211,6 +1275,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1211
1275
|
|
|
1212
1276
|
---
|
|
1213
1277
|
|
|
1278
|
+
[1.9.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.1...v1.9.2
|
|
1279
|
+
[1.9.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.0...v1.9.1
|
|
1280
|
+
[1.9.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.8.4...v1.9.0
|
|
1214
1281
|
[1.8.4]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.8.3...v1.8.4
|
|
1215
1282
|
[1.8.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.8.2...v1.8.3
|
|
1216
1283
|
[1.8.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.8.1...v1.8.2
|
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
|
+
*71 rules (64 auto-fixable) to keep your codebase clean and consistent*
|
|
23
23
|
|
|
24
24
|
</div>
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
## 🎯 Why This Plugin?
|
|
29
29
|
|
|
30
|
-
This plugin provides **
|
|
30
|
+
This plugin provides **71 custom rules** (64 auto-fixable) for code formatting. Built for **ESLint v9 flat configs**.
|
|
31
31
|
|
|
32
32
|
> **Note:** ESLint [deprecated 79 formatting rules](https://eslint.org/blog/2023/10/deprecating-formatting-rules/) in v8.53.0. Our recommended configs use `@stylistic/eslint-plugin` as the replacement for these deprecated rules.
|
|
33
33
|
|
|
@@ -36,7 +36,7 @@ This plugin provides **70 custom rules** (63 auto-fixable) for code formatting.
|
|
|
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** — 64 of 71 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
|
+
**64 rules** support automatic fixing with `eslint --fix`. 6 rules are report-only (require manual changes).
|
|
101
101
|
|
|
102
102
|
</td>
|
|
103
103
|
<td width="50%">
|
|
@@ -190,6 +190,7 @@ rules: {
|
|
|
190
190
|
"code-style/arrow-function-simplify": "error",
|
|
191
191
|
"code-style/assignment-value-same-line": "error",
|
|
192
192
|
"code-style/block-statement-newlines": "error",
|
|
193
|
+
"code-style/class-method-definition-format": "error",
|
|
193
194
|
"code-style/class-naming-convention": "error",
|
|
194
195
|
"code-style/classname-dynamic-at-end": "error",
|
|
195
196
|
"code-style/classname-multiline": "error",
|
|
@@ -260,7 +261,7 @@ rules: {
|
|
|
260
261
|
|
|
261
262
|
## 📖 Rules Categories
|
|
262
263
|
|
|
263
|
-
> **
|
|
264
|
+
> **71 rules total** — 64 with auto-fix 🔧, 7 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
|
|
264
265
|
>
|
|
265
266
|
> **Legend:** 🔧 Auto-fixable with `eslint --fix` • ⚙️ Customizable options
|
|
266
267
|
|
|
@@ -288,6 +289,7 @@ rules: {
|
|
|
288
289
|
| `component-props-destructure` | Component props must be destructured `({ prop })` not received as `(props)` 🔧 |
|
|
289
290
|
| `component-props-inline-type` | Inline type annotation `} : {` with matching props, proper spacing, commas, no interface reference 🔧 |
|
|
290
291
|
| **Class Rules** | |
|
|
292
|
+
| `class-method-definition-format` | Consistent spacing in class/method definitions: space before `{`, no space before `(` 🔧 |
|
|
291
293
|
| `class-naming-convention` | Class declarations must end with "Class" suffix (e.g., `ApiServiceClass`) 🔧 |
|
|
292
294
|
| **Control Flow Rules** | |
|
|
293
295
|
| `block-statement-newlines` | Newline after `{` and before `}` in if/for/while/function blocks 🔧 |
|
|
@@ -893,6 +895,60 @@ dispatch(
|
|
|
893
895
|
|
|
894
896
|
## 🏛️ Class Rules
|
|
895
897
|
|
|
898
|
+
### `class-method-definition-format`
|
|
899
|
+
|
|
900
|
+
**What it does:** Enforces consistent spacing in class and method definitions:
|
|
901
|
+
- Space before opening brace `{` in class declarations
|
|
902
|
+
- No space between method name and opening parenthesis `(`
|
|
903
|
+
- Space before opening brace `{` in method definitions
|
|
904
|
+
- Opening brace must be on same line as class/method signature
|
|
905
|
+
|
|
906
|
+
**Why use it:** Consistent formatting makes code more readable and prevents common spacing inconsistencies in class definitions.
|
|
907
|
+
|
|
908
|
+
```javascript
|
|
909
|
+
// ✅ Good — proper spacing in class and methods
|
|
910
|
+
class ApiServiceClass {
|
|
911
|
+
getDataHandler(): string {
|
|
912
|
+
return "data";
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async fetchUserHandler(id: string): Promise<User> {
|
|
916
|
+
return await this.fetch(id);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ❌ Bad — missing space before { in class
|
|
921
|
+
class ApiServiceClass{
|
|
922
|
+
getDataHandler(): string {
|
|
923
|
+
return "data";
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// ❌ Bad — space between method name and (
|
|
928
|
+
class ApiServiceClass {
|
|
929
|
+
getDataHandler (): string {
|
|
930
|
+
return "data";
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ❌ Bad — missing space before { in method
|
|
935
|
+
class ApiServiceClass {
|
|
936
|
+
getDataHandler(): string{
|
|
937
|
+
return "data";
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ❌ Bad — opening brace on different line
|
|
942
|
+
class ApiServiceClass {
|
|
943
|
+
getDataHandler(): string
|
|
944
|
+
{
|
|
945
|
+
return "data";
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
896
952
|
### `class-naming-convention`
|
|
897
953
|
|
|
898
954
|
**What it does:** Enforces that class declarations must end with "Class" suffix. This distinguishes class definitions from other PascalCase names like React components or type definitions.
|
|
@@ -3367,10 +3423,19 @@ const YetAnotherBad = ({ title }) => {
|
|
|
3367
3423
|
|
|
3368
3424
|
### `no-hardcoded-strings`
|
|
3369
3425
|
|
|
3370
|
-
**What it does:** Enforces that user-facing strings should be imported from constants/strings modules rather than hardcoded inline. This promotes maintainability, consistency, and enables easier internationalization.
|
|
3426
|
+
**What it does:** Enforces that user-facing strings should be imported from constants/strings/data modules rather than hardcoded inline. This promotes maintainability, consistency, and enables easier internationalization.
|
|
3371
3427
|
|
|
3372
3428
|
**Why use it:** Hardcoded strings scattered throughout your codebase are hard to maintain, translate, and keep consistent. Centralizing strings in constants makes them easy to find, update, and potentially translate.
|
|
3373
3429
|
|
|
3430
|
+
**Special detection (should be imported from `@/enums` or `@/data`):**
|
|
3431
|
+
- **HTTP status codes** — 2xx, 4xx, 5xx like "200", "404", "500"
|
|
3432
|
+
- **HTTP methods** — "GET", "POST", "PUT", "DELETE", "PATCH", etc.
|
|
3433
|
+
- **Role/permission names** — "admin", "user", "moderator", "editor", etc.
|
|
3434
|
+
- **Environment names** — "production", "development", "staging", "test", etc.
|
|
3435
|
+
- **Log levels** — "debug", "info", "warn", "error", "fatal", etc.
|
|
3436
|
+
- **Status strings** — "active", "pending", "approved", "rejected", "completed", etc.
|
|
3437
|
+
- **Priority levels** — "high", "medium", "low", "critical", "urgent", etc.
|
|
3438
|
+
|
|
3374
3439
|
**Options:**
|
|
3375
3440
|
|
|
3376
3441
|
| Option | Type | Default | Description |
|
|
@@ -3387,8 +3452,9 @@ const YetAnotherBad = ({ title }) => {
|
|
|
3387
3452
|
// ✅ Good — strings imported from constants
|
|
3388
3453
|
import { BUTTON_LABEL, ERROR_MESSAGE, welcomeText } from "@/constants";
|
|
3389
3454
|
import { FORM_LABELS } from "@/strings";
|
|
3455
|
+
import { HttpStatus, UserRole } from "@/enums";
|
|
3390
3456
|
|
|
3391
|
-
const
|
|
3457
|
+
const ComponentHandler = () => (
|
|
3392
3458
|
<div>
|
|
3393
3459
|
<button>{BUTTON_LABEL}</button>
|
|
3394
3460
|
<span>{ERROR_MESSAGE}</span>
|
|
@@ -3396,7 +3462,11 @@ const Component = () => (
|
|
|
3396
3462
|
</div>
|
|
3397
3463
|
);
|
|
3398
3464
|
|
|
3399
|
-
const
|
|
3465
|
+
const getMessageHandler = () => ERROR_MESSAGE;
|
|
3466
|
+
|
|
3467
|
+
// ✅ Good — using enums for status codes and roles
|
|
3468
|
+
if (status === HttpStatus.NOT_FOUND) { ... }
|
|
3469
|
+
if (role === UserRole.ADMIN) { ... }
|
|
3400
3470
|
|
|
3401
3471
|
// ✅ Good — technical strings are allowed
|
|
3402
3472
|
<input type="text" className="input-field" />
|
|
@@ -3409,6 +3479,10 @@ const size = "100px";
|
|
|
3409
3479
|
<span>Something went wrong</span>
|
|
3410
3480
|
const message = "Welcome to the application";
|
|
3411
3481
|
return "User not found";
|
|
3482
|
+
|
|
3483
|
+
// ❌ Bad — hardcoded status codes and roles
|
|
3484
|
+
if (status === "404") { ... }
|
|
3485
|
+
if (role === "admin") { ... }
|
|
3412
3486
|
```
|
|
3413
3487
|
|
|
3414
3488
|
**Configuration example:**
|
|
@@ -3421,10 +3495,14 @@ return "User not found";
|
|
|
3421
3495
|
}]
|
|
3422
3496
|
```
|
|
3423
3497
|
|
|
3424
|
-
**Valid import paths for
|
|
3425
|
-
- `@/
|
|
3498
|
+
**Valid import paths for strings:**
|
|
3499
|
+
- `@/data`
|
|
3426
3500
|
- `@/strings` or `@/@strings`
|
|
3427
|
-
- `@/
|
|
3501
|
+
- `@/constants` or `@/@constants`
|
|
3502
|
+
|
|
3503
|
+
**Valid import paths for enums (status codes, roles):**
|
|
3504
|
+
- `@/enums`
|
|
3505
|
+
- `@/data`
|
|
3428
3506
|
|
|
3429
3507
|
---
|
|
3430
3508
|
|
|
@@ -3463,7 +3541,7 @@ const UseAuth = () => {}; // hooks should be camelCase
|
|
|
3463
3541
|
|
|
3464
3542
|
## 🔧 Auto-fixing
|
|
3465
3543
|
|
|
3466
|
-
|
|
3544
|
+
64 of 71 rules support auto-fixing. Run ESLint with the `--fix` flag:
|
|
3467
3545
|
|
|
3468
3546
|
```bash
|
|
3469
3547
|
# Fix all files in src directory
|
package/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type RuleNames =
|
|
|
13
13
|
| "code-style/arrow-function-simplify"
|
|
14
14
|
| "code-style/assignment-value-same-line"
|
|
15
15
|
| "code-style/block-statement-newlines"
|
|
16
|
+
| "code-style/class-method-definition-format"
|
|
16
17
|
| "code-style/class-naming-convention"
|
|
17
18
|
| "code-style/comment-format"
|
|
18
19
|
| "code-style/component-props-destructure"
|
|
@@ -105,6 +106,7 @@ interface PluginRules {
|
|
|
105
106
|
"arrow-function-simplify": Rule.RuleModule;
|
|
106
107
|
"assignment-value-same-line": Rule.RuleModule;
|
|
107
108
|
"block-statement-newlines": Rule.RuleModule;
|
|
109
|
+
"class-method-definition-format": Rule.RuleModule;
|
|
108
110
|
"class-naming-convention": Rule.RuleModule;
|
|
109
111
|
"comment-format": Rule.RuleModule;
|
|
110
112
|
"component-props-destructure": Rule.RuleModule;
|
package/index.js
CHANGED
|
@@ -1850,31 +1850,80 @@ const functionCallSpacing = {
|
|
|
1850
1850
|
create(context) {
|
|
1851
1851
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
1852
1852
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1853
|
+
const checkCallSpacingHandler = (node, callee) => {
|
|
1854
|
+
// Get the last token of the callee (function name or member expression)
|
|
1855
|
+
const calleeLastToken = sourceCode.getLastToken(callee);
|
|
1856
1856
|
|
|
1857
|
-
|
|
1858
|
-
|
|
1857
|
+
// For calls with type arguments (generics), check space before <
|
|
1858
|
+
// e.g., axiosClient.get <Type>() should be axiosClient.get<Type>()
|
|
1859
|
+
if (node.typeArguments || node.typeParameters) {
|
|
1860
|
+
const typeArgs = node.typeArguments || node.typeParameters;
|
|
1861
|
+
const openAngle = sourceCode.getFirstToken(typeArgs);
|
|
1859
1862
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1863
|
+
if (openAngle && openAngle.value === "<") {
|
|
1864
|
+
const textBeforeAngle = sourceCode.text.slice(calleeLastToken.range[1], openAngle.range[0]);
|
|
1862
1865
|
|
|
1863
|
-
|
|
1866
|
+
if (/\s/.test(textBeforeAngle)) {
|
|
1867
|
+
context.report({
|
|
1868
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
1869
|
+
[calleeLastToken.range[1], openAngle.range[0]],
|
|
1870
|
+
"",
|
|
1871
|
+
),
|
|
1872
|
+
message: "No space between function name and generic type arguments",
|
|
1873
|
+
node: openAngle,
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1864
1877
|
|
|
1865
|
-
//
|
|
1866
|
-
const
|
|
1878
|
+
// Also check space between closing > and opening (
|
|
1879
|
+
const closeAngle = sourceCode.getLastToken(typeArgs);
|
|
1880
|
+
const openParen = sourceCode.getTokenAfter(typeArgs);
|
|
1867
1881
|
|
|
1868
|
-
if (
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1882
|
+
if (openParen && openParen.value === "(") {
|
|
1883
|
+
const textBetween = sourceCode.text.slice(closeAngle.range[1], openParen.range[0]);
|
|
1884
|
+
|
|
1885
|
+
if (/\s/.test(textBetween)) {
|
|
1886
|
+
context.report({
|
|
1887
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
1888
|
+
[closeAngle.range[1], openParen.range[0]],
|
|
1889
|
+
"",
|
|
1890
|
+
),
|
|
1891
|
+
message: "No space between generic type arguments and opening parenthesis",
|
|
1892
|
+
node: openParen,
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1877
1895
|
}
|
|
1896
|
+
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// Get the opening parenthesis
|
|
1901
|
+
const openParen = sourceCode.getTokenAfter(callee);
|
|
1902
|
+
|
|
1903
|
+
if (!openParen || openParen.value !== "(") return;
|
|
1904
|
+
|
|
1905
|
+
// Check if there's space between callee and opening paren
|
|
1906
|
+
const textBetween = sourceCode.text.slice(calleeLastToken.range[1], openParen.range[0]);
|
|
1907
|
+
|
|
1908
|
+
if (textBetween.length > 0) {
|
|
1909
|
+
context.report({
|
|
1910
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
1911
|
+
[calleeLastToken.range[1], openParen.range[0]],
|
|
1912
|
+
"",
|
|
1913
|
+
),
|
|
1914
|
+
message: "No space between function name and opening parenthesis",
|
|
1915
|
+
node: openParen,
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
return {
|
|
1921
|
+
CallExpression(node) {
|
|
1922
|
+
checkCallSpacingHandler(node, node.callee);
|
|
1923
|
+
},
|
|
1924
|
+
|
|
1925
|
+
NewExpression(node) {
|
|
1926
|
+
checkCallSpacingHandler(node, node.callee);
|
|
1878
1927
|
},
|
|
1879
1928
|
};
|
|
1880
1929
|
},
|
|
@@ -5384,6 +5433,180 @@ const classNamingConvention = {
|
|
|
5384
5433
|
},
|
|
5385
5434
|
};
|
|
5386
5435
|
|
|
5436
|
+
/**
|
|
5437
|
+
* ───────────────────────────────────────────────────────────────
|
|
5438
|
+
* Rule: Class/Method Definition Format
|
|
5439
|
+
* ───────────────────────────────────────────────────────────────
|
|
5440
|
+
*
|
|
5441
|
+
* Description:
|
|
5442
|
+
* Enforce consistent spacing in class and method definitions:
|
|
5443
|
+
* - Space before opening brace { in class declarations
|
|
5444
|
+
* - No space between method name and opening parenthesis (
|
|
5445
|
+
* - Space before opening brace { in method definitions
|
|
5446
|
+
* - Opening brace must be on same line as method signature
|
|
5447
|
+
*
|
|
5448
|
+
* ✓ Good:
|
|
5449
|
+
* class ApiServiceClass {
|
|
5450
|
+
* getDataHandler(): string {
|
|
5451
|
+
* return "data";
|
|
5452
|
+
* }
|
|
5453
|
+
* async fetchUserHandler(id: string): Promise<User> {
|
|
5454
|
+
* return await fetch(id);
|
|
5455
|
+
* }
|
|
5456
|
+
* }
|
|
5457
|
+
*
|
|
5458
|
+
* ✗ Bad:
|
|
5459
|
+
* class ApiServiceClass{ // Missing space before {
|
|
5460
|
+
* class ApiServiceClass
|
|
5461
|
+
* { // Opening brace on different line
|
|
5462
|
+
* getDataHandler (): string { // Space before (
|
|
5463
|
+
* getDataHandler(): string{ // Missing space before {
|
|
5464
|
+
* getDataHandler(): string
|
|
5465
|
+
* { // Opening brace on different line
|
|
5466
|
+
*/
|
|
5467
|
+
const classMethodDefinitionFormat = {
|
|
5468
|
+
create(context) {
|
|
5469
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
5470
|
+
|
|
5471
|
+
return {
|
|
5472
|
+
ClassDeclaration(node) {
|
|
5473
|
+
const classBody = node.body;
|
|
5474
|
+
|
|
5475
|
+
if (!classBody) return;
|
|
5476
|
+
|
|
5477
|
+
// Find the opening brace of the class body
|
|
5478
|
+
const openBrace = sourceCode.getFirstToken(classBody);
|
|
5479
|
+
|
|
5480
|
+
if (!openBrace || openBrace.value !== "{") return;
|
|
5481
|
+
|
|
5482
|
+
// Get the token before the opening brace (class name or extends clause)
|
|
5483
|
+
const tokenBefore = sourceCode.getTokenBefore(openBrace);
|
|
5484
|
+
|
|
5485
|
+
if (!tokenBefore) return;
|
|
5486
|
+
|
|
5487
|
+
// Check if opening brace is on same line as class declaration
|
|
5488
|
+
if (tokenBefore.loc.end.line !== openBrace.loc.start.line) {
|
|
5489
|
+
context.report({
|
|
5490
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
5491
|
+
[tokenBefore.range[1], openBrace.range[0]],
|
|
5492
|
+
" ",
|
|
5493
|
+
),
|
|
5494
|
+
message: "Opening brace should be on the same line as class declaration",
|
|
5495
|
+
node: openBrace,
|
|
5496
|
+
});
|
|
5497
|
+
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5501
|
+
// Check for space before opening brace
|
|
5502
|
+
const textBetween = sourceCode.text.slice(tokenBefore.range[1], openBrace.range[0]);
|
|
5503
|
+
|
|
5504
|
+
if (textBetween !== " ") {
|
|
5505
|
+
context.report({
|
|
5506
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
5507
|
+
[tokenBefore.range[1], openBrace.range[0]],
|
|
5508
|
+
" ",
|
|
5509
|
+
),
|
|
5510
|
+
message: "Expected single space before opening brace in class declaration",
|
|
5511
|
+
node: openBrace,
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5514
|
+
},
|
|
5515
|
+
|
|
5516
|
+
MethodDefinition(node) {
|
|
5517
|
+
const methodKey = node.key;
|
|
5518
|
+
const methodValue = node.value;
|
|
5519
|
+
|
|
5520
|
+
if (!methodKey || !methodValue) return;
|
|
5521
|
+
|
|
5522
|
+
// Check for space between method name and opening parenthesis
|
|
5523
|
+
// For computed properties, we need to get the ] token
|
|
5524
|
+
const keyLastToken = node.computed
|
|
5525
|
+
? sourceCode.getTokenAfter(methodKey, { filter: (t) => t.value === "]" })
|
|
5526
|
+
: sourceCode.getLastToken(methodKey);
|
|
5527
|
+
|
|
5528
|
+
if (!keyLastToken) return;
|
|
5529
|
+
|
|
5530
|
+
// Find the opening parenthesis of the parameters
|
|
5531
|
+
let openParen = sourceCode.getTokenAfter(keyLastToken);
|
|
5532
|
+
|
|
5533
|
+
// Skip over async, static, get, set keywords and * for generators
|
|
5534
|
+
while (openParen && openParen.value !== "(") {
|
|
5535
|
+
openParen = sourceCode.getTokenAfter(openParen);
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5538
|
+
if (!openParen || openParen.value !== "(") return;
|
|
5539
|
+
|
|
5540
|
+
// Get the token immediately before the opening paren (method name or modifier)
|
|
5541
|
+
const tokenBeforeParen = sourceCode.getTokenBefore(openParen);
|
|
5542
|
+
|
|
5543
|
+
if (tokenBeforeParen) {
|
|
5544
|
+
const textBeforeParen = sourceCode.text.slice(tokenBeforeParen.range[1], openParen.range[0]);
|
|
5545
|
+
|
|
5546
|
+
if (/\s/.test(textBeforeParen)) {
|
|
5547
|
+
context.report({
|
|
5548
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
5549
|
+
[tokenBeforeParen.range[1], openParen.range[0]],
|
|
5550
|
+
"",
|
|
5551
|
+
),
|
|
5552
|
+
message: "No space between method name and opening parenthesis",
|
|
5553
|
+
node: openParen,
|
|
5554
|
+
});
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5558
|
+
// Check for space before opening brace and brace on same line
|
|
5559
|
+
const functionBody = methodValue.body;
|
|
5560
|
+
|
|
5561
|
+
if (!functionBody || functionBody.type !== "BlockStatement") return;
|
|
5562
|
+
|
|
5563
|
+
const openBrace = sourceCode.getFirstToken(functionBody);
|
|
5564
|
+
|
|
5565
|
+
if (!openBrace || openBrace.value !== "{") return;
|
|
5566
|
+
|
|
5567
|
+
// Get the token before the opening brace (return type annotation or closing paren)
|
|
5568
|
+
const tokenBeforeBrace = sourceCode.getTokenBefore(openBrace);
|
|
5569
|
+
|
|
5570
|
+
if (!tokenBeforeBrace) return;
|
|
5571
|
+
|
|
5572
|
+
// Check if opening brace is on same line
|
|
5573
|
+
if (tokenBeforeBrace.loc.end.line !== openBrace.loc.start.line) {
|
|
5574
|
+
context.report({
|
|
5575
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
5576
|
+
[tokenBeforeBrace.range[1], openBrace.range[0]],
|
|
5577
|
+
" ",
|
|
5578
|
+
),
|
|
5579
|
+
message: "Opening brace should be on the same line as method signature",
|
|
5580
|
+
node: openBrace,
|
|
5581
|
+
});
|
|
5582
|
+
|
|
5583
|
+
return;
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
// Check for space before opening brace
|
|
5587
|
+
const textBeforeBrace = sourceCode.text.slice(tokenBeforeBrace.range[1], openBrace.range[0]);
|
|
5588
|
+
|
|
5589
|
+
if (textBeforeBrace !== " ") {
|
|
5590
|
+
context.report({
|
|
5591
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
5592
|
+
[tokenBeforeBrace.range[1], openBrace.range[0]],
|
|
5593
|
+
" ",
|
|
5594
|
+
),
|
|
5595
|
+
message: "Expected single space before opening brace in method definition",
|
|
5596
|
+
node: openBrace,
|
|
5597
|
+
});
|
|
5598
|
+
}
|
|
5599
|
+
},
|
|
5600
|
+
};
|
|
5601
|
+
},
|
|
5602
|
+
meta: {
|
|
5603
|
+
docs: { description: "Enforce consistent spacing in class and method definitions" },
|
|
5604
|
+
fixable: "whitespace",
|
|
5605
|
+
schema: [],
|
|
5606
|
+
type: "layout",
|
|
5607
|
+
},
|
|
5608
|
+
};
|
|
5609
|
+
|
|
5387
5610
|
/**
|
|
5388
5611
|
* ───────────────────────────────────────────────────────────────
|
|
5389
5612
|
* Rule: Enum Type Enforcement
|
|
@@ -9921,44 +10144,71 @@ const memberExpressionBracketSpacing = {
|
|
|
9921
10144
|
create(context) {
|
|
9922
10145
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
9923
10146
|
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
if (!node.computed) return;
|
|
10147
|
+
const checkBracketSpacingHandler = (node, objectPart, indexPart) => {
|
|
10148
|
+
const openBracket = sourceCode.getTokenBefore(indexPart);
|
|
10149
|
+
const closeBracket = sourceCode.getTokenAfter(indexPart);
|
|
9928
10150
|
|
|
9929
|
-
|
|
9930
|
-
|
|
10151
|
+
if (!openBracket || openBracket.value !== "[") return;
|
|
10152
|
+
if (!closeBracket || closeBracket.value !== "]") return;
|
|
9931
10153
|
|
|
9932
|
-
|
|
9933
|
-
|
|
10154
|
+
// Check for space before [ (between object and bracket)
|
|
10155
|
+
const objectLastToken = sourceCode.getLastToken(objectPart);
|
|
9934
10156
|
|
|
9935
|
-
|
|
9936
|
-
const
|
|
10157
|
+
if (objectLastToken) {
|
|
10158
|
+
const textBeforeOpen = sourceCode.text.slice(objectLastToken.range[1], openBracket.range[0]);
|
|
9937
10159
|
|
|
9938
|
-
if (
|
|
10160
|
+
if (/\s/.test(textBeforeOpen)) {
|
|
9939
10161
|
context.report({
|
|
9940
10162
|
fix: (fixer) => fixer.replaceTextRange(
|
|
9941
|
-
[
|
|
10163
|
+
[objectLastToken.range[1], openBracket.range[0]],
|
|
9942
10164
|
"",
|
|
9943
10165
|
),
|
|
9944
|
-
message: "No space
|
|
10166
|
+
message: "No space before opening bracket in member expression",
|
|
9945
10167
|
node: openBracket,
|
|
9946
10168
|
});
|
|
9947
10169
|
}
|
|
10170
|
+
}
|
|
9948
10171
|
|
|
9949
|
-
|
|
9950
|
-
|
|
10172
|
+
// Check for space after [
|
|
10173
|
+
const textAfterOpen = sourceCode.text.slice(openBracket.range[1], indexPart.range[0]);
|
|
9951
10174
|
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
10175
|
+
if (textAfterOpen.includes(" ") || textAfterOpen.includes("\n")) {
|
|
10176
|
+
context.report({
|
|
10177
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
10178
|
+
[openBracket.range[1], indexPart.range[0]],
|
|
10179
|
+
"",
|
|
10180
|
+
),
|
|
10181
|
+
message: "No space after opening bracket in member expression",
|
|
10182
|
+
node: openBracket,
|
|
10183
|
+
});
|
|
10184
|
+
}
|
|
10185
|
+
|
|
10186
|
+
// Check for space before ]
|
|
10187
|
+
const textBeforeClose = sourceCode.text.slice(indexPart.range[1], closeBracket.range[0]);
|
|
10188
|
+
|
|
10189
|
+
if (textBeforeClose.includes(" ") || textBeforeClose.includes("\n")) {
|
|
10190
|
+
context.report({
|
|
10191
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
10192
|
+
[indexPart.range[1], closeBracket.range[0]],
|
|
10193
|
+
"",
|
|
10194
|
+
),
|
|
10195
|
+
message: "No space before closing bracket in member expression",
|
|
10196
|
+
node: closeBracket,
|
|
10197
|
+
});
|
|
10198
|
+
}
|
|
10199
|
+
};
|
|
10200
|
+
|
|
10201
|
+
return {
|
|
10202
|
+
MemberExpression(node) {
|
|
10203
|
+
// Only check computed member expressions (bracket notation)
|
|
10204
|
+
if (!node.computed) return;
|
|
10205
|
+
|
|
10206
|
+
checkBracketSpacingHandler(node, node.object, node.property);
|
|
10207
|
+
},
|
|
10208
|
+
|
|
10209
|
+
// Handle TypeScript indexed access types: Type["prop"]
|
|
10210
|
+
TSIndexedAccessType(node) {
|
|
10211
|
+
checkBracketSpacingHandler(node, node.objectType, node.indexType);
|
|
9962
10212
|
},
|
|
9963
10213
|
};
|
|
9964
10214
|
},
|
|
@@ -13572,10 +13822,30 @@ const stringPropertySpacing = {
|
|
|
13572
13822
|
*
|
|
13573
13823
|
* Description:
|
|
13574
13824
|
* Enforces that user-facing strings should be imported from
|
|
13575
|
-
* constants/strings modules rather than hardcoded inline.
|
|
13825
|
+
* constants/strings/data modules rather than hardcoded inline.
|
|
13576
13826
|
* This promotes maintainability, consistency, and enables
|
|
13577
13827
|
* easier internationalization.
|
|
13578
13828
|
*
|
|
13829
|
+
* The rule also detects special strings that should be enums:
|
|
13830
|
+
* - HTTP status codes (2xx, 4xx, 5xx like "200", "404", "500")
|
|
13831
|
+
* - HTTP methods ("GET", "POST", "PUT", "DELETE", etc.)
|
|
13832
|
+
* - Role/permission names ("admin", "user", "moderator", etc.)
|
|
13833
|
+
* - Environment names ("production", "development", "staging", etc.)
|
|
13834
|
+
* - Log levels ("debug", "info", "warn", "error", etc.)
|
|
13835
|
+
* - Status strings ("active", "pending", "approved", "rejected", etc.)
|
|
13836
|
+
* - Priority levels ("high", "medium", "low", "critical", etc.)
|
|
13837
|
+
*
|
|
13838
|
+
* Valid import sources for strings:
|
|
13839
|
+
* - @/data
|
|
13840
|
+
* - @/strings
|
|
13841
|
+
* - @/constants
|
|
13842
|
+
* - @/@strings
|
|
13843
|
+
* - @/@constants
|
|
13844
|
+
*
|
|
13845
|
+
* Valid import sources for enums:
|
|
13846
|
+
* - @/enums
|
|
13847
|
+
* - @/data
|
|
13848
|
+
*
|
|
13579
13849
|
* Options:
|
|
13580
13850
|
* { ignoreAttributes: ["className", "id", ...] } - JSX attributes to ignore (replaces defaults)
|
|
13581
13851
|
* { extraIgnoreAttributes: ["tooltip", ...] } - Additional JSX attributes to ignore (extends defaults)
|
|
@@ -13584,16 +13854,21 @@ const stringPropertySpacing = {
|
|
|
13584
13854
|
* ✓ Good:
|
|
13585
13855
|
* import { BUTTON_LABEL, ERROR_MESSAGE } from "@/constants";
|
|
13586
13856
|
* import { welcomeText } from "@/strings";
|
|
13857
|
+
* import { HttpStatus, UserRole } from "@/enums";
|
|
13587
13858
|
*
|
|
13588
13859
|
* <button>{BUTTON_LABEL}</button>
|
|
13589
13860
|
* <span>{ERROR_MESSAGE}</span>
|
|
13590
13861
|
* const message = welcomeText;
|
|
13862
|
+
* if (status === HttpStatus.NOT_FOUND) { ... }
|
|
13863
|
+
* if (role === UserRole.ADMIN) { ... }
|
|
13591
13864
|
*
|
|
13592
13865
|
* ✗ Bad:
|
|
13593
13866
|
* <button>Submit</button>
|
|
13594
13867
|
* <span>Something went wrong</span>
|
|
13595
13868
|
* const message = "Welcome to the app";
|
|
13596
13869
|
* return "User not found";
|
|
13870
|
+
* if (status === "404") { ... } // HTTP status code
|
|
13871
|
+
* if (role === "admin") { ... } // Role name
|
|
13597
13872
|
*/
|
|
13598
13873
|
const noHardcodedStrings = {
|
|
13599
13874
|
create(context) {
|
|
@@ -13786,8 +14061,6 @@ const noHardcodedStrings = {
|
|
|
13786
14061
|
/^[A-Z][A-Z0-9_]*$/,
|
|
13787
14062
|
// Common technical strings
|
|
13788
14063
|
/^(true|false|null|undefined|NaN|Infinity)$/,
|
|
13789
|
-
// HTTP methods
|
|
13790
|
-
/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|CONNECT|TRACE)$/,
|
|
13791
14064
|
// Content types
|
|
13792
14065
|
/^application\//,
|
|
13793
14066
|
// Environment variables pattern
|
|
@@ -13816,16 +14089,181 @@ const noHardcodedStrings = {
|
|
|
13816
14089
|
|
|
13817
14090
|
const allIgnorePatterns = [...technicalPatterns, ...extraIgnorePatterns];
|
|
13818
14091
|
|
|
13819
|
-
//
|
|
13820
|
-
const
|
|
14092
|
+
// HTTP status codes that should NOT be hardcoded (2xx, 4xx, and 5xx)
|
|
14093
|
+
const httpStatusCodePattern = /^[245]\d{2}$/;
|
|
14094
|
+
|
|
14095
|
+
// Common role/permission names that should be imported from enums/data
|
|
14096
|
+
const rolePermissionNames = new Set([
|
|
14097
|
+
"admin",
|
|
14098
|
+
"administrator",
|
|
14099
|
+
"editor",
|
|
14100
|
+
"guest",
|
|
14101
|
+
"manager",
|
|
14102
|
+
"member",
|
|
14103
|
+
"moderator",
|
|
14104
|
+
"operator",
|
|
14105
|
+
"owner",
|
|
14106
|
+
"reviewer",
|
|
14107
|
+
"subscriber",
|
|
14108
|
+
"superadmin",
|
|
14109
|
+
"supervisor",
|
|
14110
|
+
"user",
|
|
14111
|
+
"viewer",
|
|
14112
|
+
]);
|
|
14113
|
+
|
|
14114
|
+
// HTTP methods that should be imported from enums/data
|
|
14115
|
+
const httpMethods = new Set([
|
|
14116
|
+
"CONNECT",
|
|
14117
|
+
"DELETE",
|
|
14118
|
+
"GET",
|
|
14119
|
+
"HEAD",
|
|
14120
|
+
"OPTIONS",
|
|
14121
|
+
"PATCH",
|
|
14122
|
+
"POST",
|
|
14123
|
+
"PUT",
|
|
14124
|
+
"TRACE",
|
|
14125
|
+
]);
|
|
14126
|
+
|
|
14127
|
+
// Environment names that should be imported from enums/data
|
|
14128
|
+
const environmentNames = new Set([
|
|
14129
|
+
"dev",
|
|
14130
|
+
"development",
|
|
14131
|
+
"local",
|
|
14132
|
+
"prod",
|
|
14133
|
+
"production",
|
|
14134
|
+
"qa",
|
|
14135
|
+
"sandbox",
|
|
14136
|
+
"staging",
|
|
14137
|
+
"test",
|
|
14138
|
+
"testing",
|
|
14139
|
+
"uat",
|
|
14140
|
+
]);
|
|
14141
|
+
|
|
14142
|
+
// Log levels that should be imported from enums/data
|
|
14143
|
+
const logLevels = new Set([
|
|
14144
|
+
"debug",
|
|
14145
|
+
"error",
|
|
14146
|
+
"fatal",
|
|
14147
|
+
"info",
|
|
14148
|
+
"log",
|
|
14149
|
+
"trace",
|
|
14150
|
+
"warn",
|
|
14151
|
+
"warning",
|
|
14152
|
+
]);
|
|
13821
14153
|
|
|
13822
|
-
//
|
|
14154
|
+
// Status/state strings that should be imported from enums/data
|
|
14155
|
+
const statusStrings = new Set([
|
|
14156
|
+
"accepted",
|
|
14157
|
+
"active",
|
|
14158
|
+
"approved",
|
|
14159
|
+
"archived",
|
|
14160
|
+
"blocked",
|
|
14161
|
+
"cancelled",
|
|
14162
|
+
"closed",
|
|
14163
|
+
"completed",
|
|
14164
|
+
"declined",
|
|
14165
|
+
"deleted",
|
|
14166
|
+
"disabled",
|
|
14167
|
+
"draft",
|
|
14168
|
+
"enabled",
|
|
14169
|
+
"expired",
|
|
14170
|
+
"failed",
|
|
14171
|
+
"inactive",
|
|
14172
|
+
"open",
|
|
14173
|
+
"paused",
|
|
14174
|
+
"pending",
|
|
14175
|
+
"processing",
|
|
14176
|
+
"published",
|
|
14177
|
+
"rejected",
|
|
14178
|
+
"resolved",
|
|
14179
|
+
"scheduled",
|
|
14180
|
+
"submitted",
|
|
14181
|
+
"success",
|
|
14182
|
+
"suspended",
|
|
14183
|
+
"verified",
|
|
14184
|
+
]);
|
|
14185
|
+
|
|
14186
|
+
// Priority levels that should be imported from enums/data
|
|
14187
|
+
const priorityLevels = new Set([
|
|
14188
|
+
"critical",
|
|
14189
|
+
"high",
|
|
14190
|
+
"highest",
|
|
14191
|
+
"low",
|
|
14192
|
+
"lowest",
|
|
14193
|
+
"medium",
|
|
14194
|
+
"normal",
|
|
14195
|
+
"urgent",
|
|
14196
|
+
]);
|
|
14197
|
+
|
|
14198
|
+
// Check functions for each category
|
|
14199
|
+
const isHttpStatusCodeHandler = (str) => httpStatusCodePattern.test(str);
|
|
14200
|
+
const isRoleNameHandler = (str) => rolePermissionNames.has(str.toLowerCase());
|
|
14201
|
+
const isHttpMethodHandler = (str) => httpMethods.has(str.toUpperCase());
|
|
14202
|
+
const isEnvironmentNameHandler = (str) => environmentNames.has(str.toLowerCase());
|
|
14203
|
+
const isLogLevelHandler = (str) => logLevels.has(str.toLowerCase());
|
|
14204
|
+
const isStatusStringHandler = (str) => statusStrings.has(str.toLowerCase());
|
|
14205
|
+
const isPriorityLevelHandler = (str) => priorityLevels.has(str.toLowerCase());
|
|
14206
|
+
|
|
14207
|
+
// Check if string should be flagged even if it matches technical patterns
|
|
14208
|
+
const isFlaggedSpecialStringHandler = (str) => isHttpStatusCodeHandler(str)
|
|
14209
|
+
|| isRoleNameHandler(str)
|
|
14210
|
+
|| isHttpMethodHandler(str)
|
|
14211
|
+
|| isEnvironmentNameHandler(str)
|
|
14212
|
+
|| isLogLevelHandler(str)
|
|
14213
|
+
|| isStatusStringHandler(str)
|
|
14214
|
+
|| isPriorityLevelHandler(str);
|
|
14215
|
+
|
|
14216
|
+
// Get descriptive error message based on string type
|
|
14217
|
+
const getErrorMessageHandler = (str, context = "") => {
|
|
14218
|
+
const truncatedStr = str.length > 30 ? `${str.substring(0, 30)}...` : str;
|
|
14219
|
+
const contextPart = context ? ` in ${context}` : "";
|
|
14220
|
+
|
|
14221
|
+
if (isHttpStatusCodeHandler(str)) {
|
|
14222
|
+
return `Hardcoded HTTP status code "${truncatedStr}"${contextPart} should be imported from @/enums or @/data`;
|
|
14223
|
+
}
|
|
14224
|
+
|
|
14225
|
+
if (isRoleNameHandler(str)) {
|
|
14226
|
+
return `Hardcoded role/permission "${truncatedStr}"${contextPart} should be imported from @/enums or @/data`;
|
|
14227
|
+
}
|
|
14228
|
+
|
|
14229
|
+
if (isHttpMethodHandler(str)) {
|
|
14230
|
+
return `Hardcoded HTTP method "${truncatedStr}"${contextPart} should be imported from @/enums or @/data`;
|
|
14231
|
+
}
|
|
14232
|
+
|
|
14233
|
+
if (isEnvironmentNameHandler(str)) {
|
|
14234
|
+
return `Hardcoded environment name "${truncatedStr}"${contextPart} should be imported from @/enums or @/data`;
|
|
14235
|
+
}
|
|
14236
|
+
|
|
14237
|
+
if (isLogLevelHandler(str)) {
|
|
14238
|
+
return `Hardcoded log level "${truncatedStr}"${contextPart} should be imported from @/enums or @/data`;
|
|
14239
|
+
}
|
|
14240
|
+
|
|
14241
|
+
if (isStatusStringHandler(str)) {
|
|
14242
|
+
return `Hardcoded status "${truncatedStr}"${contextPart} should be imported from @/enums or @/data`;
|
|
14243
|
+
}
|
|
14244
|
+
|
|
14245
|
+
if (isPriorityLevelHandler(str)) {
|
|
14246
|
+
return `Hardcoded priority level "${truncatedStr}"${contextPart} should be imported from @/enums or @/data`;
|
|
14247
|
+
}
|
|
14248
|
+
|
|
14249
|
+
return `Hardcoded string "${truncatedStr}"${contextPart} should be imported from @/data or @/strings or @/constants or @/@constants or @/@strings`;
|
|
14250
|
+
};
|
|
14251
|
+
|
|
14252
|
+
// Check if a string matches any ignore pattern (but not if it's a flagged special string)
|
|
14253
|
+
const shouldIgnoreStringHandler = (str) => {
|
|
14254
|
+
// Always flag HTTP status codes and role names
|
|
14255
|
+
if (isFlaggedSpecialStringHandler(str)) return false;
|
|
14256
|
+
|
|
14257
|
+
return allIgnorePatterns.some((pattern) => pattern.test(str));
|
|
14258
|
+
};
|
|
14259
|
+
|
|
14260
|
+
// Check if we're inside a constants/strings/data/enums file
|
|
13823
14261
|
const isConstantsFileHandler = () => {
|
|
13824
14262
|
const filename = context.filename || context.getFilename();
|
|
13825
14263
|
const normalizedPath = filename.replace(/\\/g, "/").toLowerCase();
|
|
13826
14264
|
|
|
13827
|
-
// Check if file is in constants/strings folders
|
|
13828
|
-
return /\/(constants|strings|@constants|@strings)(\/|\.)/i.test(normalizedPath)
|
|
14265
|
+
// Check if file is in constants/strings/data/enums folders
|
|
14266
|
+
return /\/(constants|strings|@constants|@strings|data|@data|enums|@enums)(\/|\.)/i.test(normalizedPath)
|
|
13829
14267
|
|| /\/data\/(constants|strings)/i.test(normalizedPath);
|
|
13830
14268
|
};
|
|
13831
14269
|
|
|
@@ -13838,8 +14276,8 @@ const noHardcodedStrings = {
|
|
|
13838
14276
|
|
|
13839
14277
|
if (typeof importPath !== "string") return;
|
|
13840
14278
|
|
|
13841
|
-
// Check if import is from constants/strings
|
|
13842
|
-
const isFromConstants = /@?\/?(@?constants|@?strings|data\/constants|data\/strings)/i
|
|
14279
|
+
// Check if import is from constants/strings/data/enums
|
|
14280
|
+
const isFromConstants = /@?\/?(@?constants|@?strings|@?data|@?enums|data\/constants|data\/strings)/i
|
|
13843
14281
|
.test(importPath);
|
|
13844
14282
|
|
|
13845
14283
|
if (isFromConstants) {
|
|
@@ -13931,9 +14369,17 @@ const noHardcodedStrings = {
|
|
|
13931
14369
|
if (current.type === "VariableDeclarator") {
|
|
13932
14370
|
const varName = current.id && current.id.name;
|
|
13933
14371
|
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
14372
|
+
if (varName) {
|
|
14373
|
+
// Check for SCREAMING_SNAKE_CASE (e.g., MY_CONSTANT, API_URL)
|
|
14374
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(varName)) {
|
|
14375
|
+
return true;
|
|
14376
|
+
}
|
|
14377
|
+
|
|
14378
|
+
// Check for exact keywords or keywords at word boundaries (not in Handler names)
|
|
14379
|
+
// Match: MESSAGES, Messages, userMessages, but NOT longMessageHandler
|
|
14380
|
+
if (/^(constants?|strings?|messages?|labels?|texts?|data)$/i.test(varName)) {
|
|
14381
|
+
return true;
|
|
14382
|
+
}
|
|
13937
14383
|
}
|
|
13938
14384
|
}
|
|
13939
14385
|
|
|
@@ -13960,13 +14406,18 @@ const noHardcodedStrings = {
|
|
|
13960
14406
|
JSXText(node) {
|
|
13961
14407
|
const text = node.value.trim();
|
|
13962
14408
|
|
|
13963
|
-
if (!text
|
|
14409
|
+
if (!text) return;
|
|
14410
|
+
|
|
14411
|
+
// Check if it's a flagged special string (status code, role name)
|
|
14412
|
+
const isSpecialString = isFlaggedSpecialStringHandler(text);
|
|
13964
14413
|
|
|
13965
|
-
|
|
13966
|
-
|
|
14414
|
+
if (!isSpecialString && shouldIgnoreStringHandler(text)) return;
|
|
14415
|
+
|
|
14416
|
+
// Check if it looks like user-facing text (contains letters) - skip for special strings
|
|
14417
|
+
if (!isSpecialString && !/[a-zA-Z]/.test(text)) return;
|
|
13967
14418
|
|
|
13968
14419
|
context.report({
|
|
13969
|
-
message:
|
|
14420
|
+
message: getErrorMessageHandler(text, "JSX"),
|
|
13970
14421
|
node,
|
|
13971
14422
|
});
|
|
13972
14423
|
},
|
|
@@ -13982,13 +14433,16 @@ const noHardcodedStrings = {
|
|
|
13982
14433
|
if (expression.type === "Literal" && typeof expression.value === "string") {
|
|
13983
14434
|
const str = expression.value;
|
|
13984
14435
|
|
|
13985
|
-
if (
|
|
14436
|
+
// Check if it's a flagged special string (status code, role name)
|
|
14437
|
+
const isSpecialString = isFlaggedSpecialStringHandler(str);
|
|
14438
|
+
|
|
14439
|
+
if (!isSpecialString && shouldIgnoreStringHandler(str)) return;
|
|
13986
14440
|
|
|
13987
|
-
// Check if it looks like user-facing text
|
|
13988
|
-
if (!/[a-zA-Z]/.test(str)) return;
|
|
14441
|
+
// Check if it looks like user-facing text - skip for special strings
|
|
14442
|
+
if (!isSpecialString && !/[a-zA-Z]/.test(str)) return;
|
|
13989
14443
|
|
|
13990
14444
|
context.report({
|
|
13991
|
-
message:
|
|
14445
|
+
message: getErrorMessageHandler(str, "JSX expression"),
|
|
13992
14446
|
node: expression,
|
|
13993
14447
|
});
|
|
13994
14448
|
}
|
|
@@ -13998,16 +14452,19 @@ const noHardcodedStrings = {
|
|
|
13998
14452
|
expression.quasis.forEach((quasi) => {
|
|
13999
14453
|
const str = quasi.value.cooked || quasi.value.raw;
|
|
14000
14454
|
|
|
14001
|
-
if (
|
|
14455
|
+
// Check if it's a flagged special string (status code, role name)
|
|
14456
|
+
const isSpecialString = isFlaggedSpecialStringHandler(str);
|
|
14002
14457
|
|
|
14003
|
-
|
|
14004
|
-
|
|
14458
|
+
if (!isSpecialString && shouldIgnoreStringHandler(str)) return;
|
|
14459
|
+
|
|
14460
|
+
// Check if it contains user-facing text - skip for special strings
|
|
14461
|
+
if (!isSpecialString && !/[a-zA-Z]{2,}/.test(str)) return;
|
|
14005
14462
|
|
|
14006
14463
|
// Skip if it looks like a path or URL pattern
|
|
14007
14464
|
if (/^[/.]|https?:\/\//.test(str)) return;
|
|
14008
14465
|
|
|
14009
14466
|
context.report({
|
|
14010
|
-
message:
|
|
14467
|
+
message: getErrorMessageHandler(str, "template literal"),
|
|
14011
14468
|
node: quasi,
|
|
14012
14469
|
});
|
|
14013
14470
|
});
|
|
@@ -14034,15 +14491,19 @@ const noHardcodedStrings = {
|
|
|
14034
14491
|
if (node.value.type === "Literal" && typeof node.value.value === "string") {
|
|
14035
14492
|
const str = node.value.value;
|
|
14036
14493
|
|
|
14037
|
-
if (
|
|
14494
|
+
// Check if it's a flagged special string (status code, role name)
|
|
14495
|
+
const isSpecialString = isFlaggedSpecialStringHandler(str);
|
|
14496
|
+
|
|
14497
|
+
if (!isSpecialString && shouldIgnoreStringHandler(str)) return;
|
|
14038
14498
|
|
|
14039
|
-
// Check if it looks like user-facing text
|
|
14040
|
-
if (!/[a-zA-Z]/.test(str)) return;
|
|
14499
|
+
// Check if it looks like user-facing text - skip for special strings
|
|
14500
|
+
if (!isSpecialString && !/[a-zA-Z]/.test(str)) return;
|
|
14041
14501
|
|
|
14042
|
-
|
|
14502
|
+
// Require multiple words or reasonable length for non-special strings
|
|
14503
|
+
if (!isSpecialString && str.split(/\s+/).length < 2 && str.length < 10) return;
|
|
14043
14504
|
|
|
14044
14505
|
context.report({
|
|
14045
|
-
message:
|
|
14506
|
+
message: getErrorMessageHandler(str, `attribute "${attrName}"`),
|
|
14046
14507
|
node: node.value,
|
|
14047
14508
|
});
|
|
14048
14509
|
}
|
|
@@ -14057,14 +14518,18 @@ const noHardcodedStrings = {
|
|
|
14057
14518
|
if (expression.type === "Literal" && typeof expression.value === "string") {
|
|
14058
14519
|
const str = expression.value;
|
|
14059
14520
|
|
|
14060
|
-
if (
|
|
14521
|
+
// Check if it's a flagged special string (status code, role name)
|
|
14522
|
+
const isSpecialString = isFlaggedSpecialStringHandler(str);
|
|
14061
14523
|
|
|
14062
|
-
if (
|
|
14524
|
+
if (!isSpecialString && shouldIgnoreStringHandler(str)) return;
|
|
14063
14525
|
|
|
14064
|
-
if (
|
|
14526
|
+
if (!isSpecialString && !/[a-zA-Z]/.test(str)) return;
|
|
14527
|
+
|
|
14528
|
+
// Require multiple words or reasonable length for non-special strings
|
|
14529
|
+
if (!isSpecialString && str.split(/\s+/).length < 2 && str.length < 10) return;
|
|
14065
14530
|
|
|
14066
14531
|
context.report({
|
|
14067
|
-
message:
|
|
14532
|
+
message: getErrorMessageHandler(str, `attribute "${attrName}"`),
|
|
14068
14533
|
node: expression,
|
|
14069
14534
|
});
|
|
14070
14535
|
}
|
|
@@ -14078,8 +14543,11 @@ const noHardcodedStrings = {
|
|
|
14078
14543
|
|
|
14079
14544
|
const str = node.value;
|
|
14080
14545
|
|
|
14081
|
-
//
|
|
14082
|
-
|
|
14546
|
+
// Check if it's a flagged special string (status code, role name)
|
|
14547
|
+
const isSpecialString = isFlaggedSpecialStringHandler(str);
|
|
14548
|
+
|
|
14549
|
+
// Skip if it matches ignore patterns (but not special strings)
|
|
14550
|
+
if (!isSpecialString && shouldIgnoreStringHandler(str)) return;
|
|
14083
14551
|
|
|
14084
14552
|
// Skip if not in relevant context
|
|
14085
14553
|
if (!isInRelevantContextHandler(node)) return;
|
|
@@ -14096,14 +14564,14 @@ const noHardcodedStrings = {
|
|
|
14096
14564
|
// Skip object property keys
|
|
14097
14565
|
if (node.parent.type === "Property" && node.parent.key === node) return;
|
|
14098
14566
|
|
|
14099
|
-
// Skip if it doesn't look like user-facing text
|
|
14100
|
-
if (!/[a-zA-Z]/.test(str)) return;
|
|
14567
|
+
// Skip if it doesn't look like user-facing text - but not for special strings
|
|
14568
|
+
if (!isSpecialString && !/[a-zA-Z]/.test(str)) return;
|
|
14101
14569
|
|
|
14102
|
-
// Require at least 2 words or be reasonably long
|
|
14103
|
-
if (str.split(/\s+/).length < 2 && str.length < 15) return;
|
|
14570
|
+
// Require at least 2 words or be reasonably long - but not for special strings
|
|
14571
|
+
if (!isSpecialString && str.split(/\s+/).length < 2 && str.length < 15) return;
|
|
14104
14572
|
|
|
14105
14573
|
context.report({
|
|
14106
|
-
message:
|
|
14574
|
+
message: getErrorMessageHandler(str),
|
|
14107
14575
|
node,
|
|
14108
14576
|
});
|
|
14109
14577
|
},
|
|
@@ -14123,10 +14591,13 @@ const noHardcodedStrings = {
|
|
|
14123
14591
|
node.quasis.forEach((quasi) => {
|
|
14124
14592
|
const str = quasi.value.cooked || quasi.value.raw;
|
|
14125
14593
|
|
|
14126
|
-
if (
|
|
14594
|
+
// Check if it's a flagged special string (status code, role name)
|
|
14595
|
+
const isSpecialString = isFlaggedSpecialStringHandler(str);
|
|
14596
|
+
|
|
14597
|
+
if (!isSpecialString && shouldIgnoreStringHandler(str)) return;
|
|
14127
14598
|
|
|
14128
|
-
// Check if it contains substantial user-facing text
|
|
14129
|
-
if (!/[a-zA-Z]{3,}/.test(str)) return;
|
|
14599
|
+
// Check if it contains substantial user-facing text - but not for special strings
|
|
14600
|
+
if (!isSpecialString && !/[a-zA-Z]{3,}/.test(str)) return;
|
|
14130
14601
|
|
|
14131
14602
|
// Skip if it looks like a path, URL, or query
|
|
14132
14603
|
if (/^[/.]|^https?:\/\/|^[?&]/.test(str)) return;
|
|
@@ -14135,7 +14606,7 @@ const noHardcodedStrings = {
|
|
|
14135
14606
|
if (node.expressions.length > node.quasis.length) return;
|
|
14136
14607
|
|
|
14137
14608
|
context.report({
|
|
14138
|
-
message:
|
|
14609
|
+
message: getErrorMessageHandler(str, "template literal"),
|
|
14139
14610
|
node: quasi,
|
|
14140
14611
|
});
|
|
14141
14612
|
});
|
|
@@ -19291,6 +19762,7 @@ export default {
|
|
|
19291
19762
|
"ternary-condition-multiline": ternaryConditionMultiline,
|
|
19292
19763
|
|
|
19293
19764
|
// Class rules
|
|
19765
|
+
"class-method-definition-format": classMethodDefinitionFormat,
|
|
19294
19766
|
"class-naming-convention": classNamingConvention,
|
|
19295
19767
|
|
|
19296
19768
|
// Function rules
|
package/package.json
CHANGED