eslint-plugin-code-style 1.10.0 → 1.11.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 +87 -1
- package/README.md +46 -6
- package/index.d.ts +2 -0
- package/index.js +373 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.11.0] - 2026-02-03
|
|
11
|
+
|
|
12
|
+
**New Rule: svg-component-icon-naming + Multiple Component Props Fixes**
|
|
13
|
+
|
|
14
|
+
**Version Range:** v1.10.1 → v1.11.0
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
**New Rules (1)**
|
|
19
|
+
|
|
20
|
+
- **`svg-component-icon-naming`** - Enforce SVG components to have 'Icon' suffix and vice versa
|
|
21
|
+
- Components returning only `<svg>` must end with "Icon" suffix (e.g., `SuccessIcon`)
|
|
22
|
+
- Components with "Icon" suffix must return an `<svg>` element
|
|
23
|
+
- Works with arrow functions and function declarations
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **`component-props-inline-type`** - Single prop trailing comma now correctly removed (was not detecting comma in member range)
|
|
28
|
+
- **`component-props-inline-type`** - Closing `})` now properly placed on its own line for multiple type props (was missing for non-intersection types)
|
|
29
|
+
- **`function-params-per-line`** - Type annotations no longer removed when collapsing params (was losing entire type annotation)
|
|
30
|
+
- **`function-params-per-line`** - Default values preserved for shorthand props (e.g., `className = ""` no longer becomes `className`)
|
|
31
|
+
- **`function-params-per-line`** - Type annotation complexity now considered (2+ type props = complex, prevents incorrect collapsing)
|
|
32
|
+
|
|
33
|
+
### Stats
|
|
34
|
+
|
|
35
|
+
- Total Rules: 73 (was 72)
|
|
36
|
+
- Auto-fixable: 65 rules
|
|
37
|
+
- Report-only: 8 rules
|
|
38
|
+
|
|
39
|
+
**Full Changelog:** [v1.10.1...v1.11.0](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.1...v1.11.0)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## [1.10.3] - 2026-02-03
|
|
44
|
+
|
|
45
|
+
**Bug Fixes: className template literals and trailing comma removal**
|
|
46
|
+
|
|
47
|
+
**Version Range:** v1.10.2 → v1.10.3
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **`no-hardcoded-strings`** - Skip template literals inside className/style attributes (Tailwind classes in template literals)
|
|
52
|
+
- **`component-props-inline-type`** - Auto-fix to REMOVE trailing comma for single property (not just skip adding it)
|
|
53
|
+
|
|
54
|
+
**Full Changelog:** [v1.10.2...v1.10.3](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.2...v1.10.3)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## [1.10.2] - 2026-02-03
|
|
59
|
+
|
|
60
|
+
**Bug Fixes: component-props-inline-type and no-hardcoded-strings**
|
|
61
|
+
|
|
62
|
+
**Version Range:** v1.10.1 → v1.10.2
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **`component-props-inline-type`** - Don't require trailing comma for single property in inline type definitions
|
|
67
|
+
- **`no-hardcoded-strings`** - Skip 40+ SVG attributes (strokeLinecap, strokeLinejoin, filter, result, in, in2, mode, colorInterpolationFilters, etc.)
|
|
68
|
+
- **`no-hardcoded-strings`** - Skip SVG standard attribute values (round, butt, square, miter, bevel, none, normal, sRGB, userSpaceOnUse, etc.)
|
|
69
|
+
- **`no-hardcoded-strings`** - Skip URL references (url(#...)) and scientific notation numbers
|
|
70
|
+
- **`no-hardcoded-strings`** - Skip CSS property values (cursor: pointer, display: flex, position: absolute, etc.)
|
|
71
|
+
- **`no-hardcoded-strings`** - Skip SVG filter result identifiers (BackgroundImageFix, SourceGraphic, etc.)
|
|
72
|
+
|
|
73
|
+
**Full Changelog:** [v1.10.1...v1.10.2](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.1...v1.10.2)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## [1.10.1] - 2026-02-03
|
|
78
|
+
|
|
79
|
+
**Bug Fix: logical-expression-multiline rule improvements**
|
|
80
|
+
|
|
81
|
+
**Version Range:** v1.10.0 → v1.10.1
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **`logical-expression-multiline`** - Add collapse to single line for simple expressions (≤3 operands)
|
|
86
|
+
- **`logical-expression-multiline`** - Skip collapsing when any operand is multiline (e.g., JSX elements)
|
|
87
|
+
|
|
88
|
+
**Full Changelog:** [v1.10.0...v1.10.1](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.0...v1.10.1)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
10
92
|
## [1.10.0] - 2026-02-03
|
|
11
93
|
|
|
12
94
|
**New Rule: logical-expression-multiline + Enhanced no-hardcoded-strings**
|
|
@@ -1379,7 +1461,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1379
1461
|
|
|
1380
1462
|
---
|
|
1381
1463
|
|
|
1382
|
-
[1.
|
|
1464
|
+
[1.11.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.3...v1.11.0
|
|
1465
|
+
[1.10.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.2...v1.10.3
|
|
1466
|
+
[1.10.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.1...v1.10.2
|
|
1467
|
+
[1.10.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.0...v1.10.1
|
|
1468
|
+
[1.10.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.7...v1.10.0
|
|
1383
1469
|
[1.9.7]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.6...v1.9.7
|
|
1384
1470
|
[1.9.6]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.5...v1.9.6
|
|
1385
1471
|
[1.9.5]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.4...v1.9.5
|
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
|
+
*73 rules (65 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 **73 custom rules** (65 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 **71 custom rules** (64 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** — 65 of 73 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
|
+
**65 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%">
|
|
@@ -199,6 +199,7 @@ rules: {
|
|
|
199
199
|
"code-style/comment-format": "error",
|
|
200
200
|
"code-style/component-props-destructure": "error",
|
|
201
201
|
"code-style/component-props-inline-type": "error",
|
|
202
|
+
"code-style/svg-component-icon-naming": "error",
|
|
202
203
|
"code-style/curried-arrow-same-line": "error",
|
|
203
204
|
"code-style/empty-line-after-block": "error",
|
|
204
205
|
"code-style/enum-format": "error",
|
|
@@ -261,7 +262,7 @@ rules: {
|
|
|
261
262
|
|
|
262
263
|
## 📖 Rules Categories
|
|
263
264
|
|
|
264
|
-
> **
|
|
265
|
+
> **73 rules total** — 65 with auto-fix 🔧, 8 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
|
|
265
266
|
>
|
|
266
267
|
> **Legend:** 🔧 Auto-fixable with `eslint --fix` • ⚙️ Customizable options
|
|
267
268
|
|
|
@@ -288,6 +289,7 @@ rules: {
|
|
|
288
289
|
| **Component Rules** | |
|
|
289
290
|
| `component-props-destructure` | Component props must be destructured `({ prop })` not received as `(props)` 🔧 |
|
|
290
291
|
| `component-props-inline-type` | Inline type annotation `} : {` with matching props, proper spacing, commas, no interface reference 🔧 |
|
|
292
|
+
| `svg-component-icon-naming` | SVG components must end with "Icon" suffix; "Icon" suffix components must return SVG |
|
|
291
293
|
| **Class Rules** | |
|
|
292
294
|
| `class-method-definition-format` | Consistent spacing in class/method definitions: space before `{`, no space before `(` 🔧 |
|
|
293
295
|
| `class-naming-convention` | Class declarations must end with "Class" suffix (e.g., `ApiServiceClass`) 🔧 |
|
|
@@ -2965,6 +2967,44 @@ export const Card = ({ a, b } : { a: string, b: string }) => (
|
|
|
2965
2967
|
);
|
|
2966
2968
|
```
|
|
2967
2969
|
|
|
2970
|
+
---
|
|
2971
|
+
|
|
2972
|
+
### `svg-component-icon-naming`
|
|
2973
|
+
|
|
2974
|
+
**What it does:** Enforces naming conventions for SVG icon components:
|
|
2975
|
+
- Components that return only an SVG element must have a name ending with "Icon"
|
|
2976
|
+
- Components with "Icon" suffix must return an SVG element
|
|
2977
|
+
|
|
2978
|
+
**Why use it:** Consistent naming makes it immediately clear which components render icons, improving code readability and making icon components easier to find in large codebases.
|
|
2979
|
+
|
|
2980
|
+
```tsx
|
|
2981
|
+
// ✅ Good — returns SVG and ends with "Icon"
|
|
2982
|
+
export const SuccessIcon = ({ className = "" }: { className?: string }) => (
|
|
2983
|
+
<svg className={className}>
|
|
2984
|
+
<path d="M9 12l2 2 4-4" />
|
|
2985
|
+
</svg>
|
|
2986
|
+
);
|
|
2987
|
+
|
|
2988
|
+
// ✅ Good — returns non-SVG and doesn't end with "Icon"
|
|
2989
|
+
export const Button = ({ children }: { children: React.ReactNode }) => (
|
|
2990
|
+
<button>{children}</button>
|
|
2991
|
+
);
|
|
2992
|
+
|
|
2993
|
+
// ❌ Bad — returns SVG but doesn't end with "Icon"
|
|
2994
|
+
export const Success = ({ className = "" }: { className?: string }) => (
|
|
2995
|
+
<svg className={className}>
|
|
2996
|
+
<path d="M9 12l2 2 4-4" />
|
|
2997
|
+
</svg>
|
|
2998
|
+
);
|
|
2999
|
+
// Error: Component "Success" returns an SVG element and should end with "Icon" suffix
|
|
3000
|
+
|
|
3001
|
+
// ❌ Bad — ends with "Icon" but doesn't return SVG
|
|
3002
|
+
export const ButtonIcon = ({ children }: { children: React.ReactNode }) => (
|
|
3003
|
+
<button>{children}</button>
|
|
3004
|
+
);
|
|
3005
|
+
// Error: Component "ButtonIcon" has "Icon" suffix but doesn't return an SVG element
|
|
3006
|
+
```
|
|
3007
|
+
|
|
2968
3008
|
<br />
|
|
2969
3009
|
|
|
2970
3010
|
## 🔷 TypeScript Rules
|
|
@@ -3541,7 +3581,7 @@ const UseAuth = () => {}; // hooks should be camelCase
|
|
|
3541
3581
|
|
|
3542
3582
|
## 🔧 Auto-fixing
|
|
3543
3583
|
|
|
3544
|
-
|
|
3584
|
+
65 of 73 rules support auto-fixing. Run ESLint with the `--fix` flag:
|
|
3545
3585
|
|
|
3546
3586
|
```bash
|
|
3547
3587
|
# Fix all files in src directory
|
package/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type RuleNames =
|
|
|
19
19
|
| "code-style/component-props-destructure"
|
|
20
20
|
| "code-style/react-code-order"
|
|
21
21
|
| "code-style/component-props-inline-type"
|
|
22
|
+
| "code-style/svg-component-icon-naming"
|
|
22
23
|
| "code-style/curried-arrow-same-line"
|
|
23
24
|
| "code-style/empty-line-after-block"
|
|
24
25
|
| "code-style/enum-type-enforcement"
|
|
@@ -113,6 +114,7 @@ interface PluginRules {
|
|
|
113
114
|
"component-props-destructure": Rule.RuleModule;
|
|
114
115
|
"react-code-order": Rule.RuleModule;
|
|
115
116
|
"component-props-inline-type": Rule.RuleModule;
|
|
117
|
+
"svg-component-icon-naming": Rule.RuleModule;
|
|
116
118
|
"curried-arrow-same-line": Rule.RuleModule;
|
|
117
119
|
"empty-line-after-block": Rule.RuleModule;
|
|
118
120
|
"enum-type-enforcement": Rule.RuleModule;
|
package/index.js
CHANGED
|
@@ -2540,10 +2540,22 @@ const functionParamsPerLine = {
|
|
|
2540
2540
|
// Check if shorthand (key and value are the same identifier)
|
|
2541
2541
|
const isShorthand = prop.shorthand || (prop.value && prop.value.type === "Identifier" && prop.value.name === prop.key.name);
|
|
2542
2542
|
|
|
2543
|
-
|
|
2543
|
+
if (isShorthand) {
|
|
2544
|
+
// For shorthand with default value, use the value (includes default)
|
|
2545
|
+
return prop.value && prop.value.type === "AssignmentPattern" ? value : key;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
return `${key}: ${value}`;
|
|
2544
2549
|
});
|
|
2545
2550
|
|
|
2546
|
-
|
|
2551
|
+
let result = `{ ${props.join(", ")} }`;
|
|
2552
|
+
|
|
2553
|
+
// Preserve type annotation if present
|
|
2554
|
+
if (param.typeAnnotation) {
|
|
2555
|
+
result += sourceCode.getText(param.typeAnnotation);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
return result;
|
|
2547
2559
|
}
|
|
2548
2560
|
|
|
2549
2561
|
if (param.type === "ArrayPattern") {
|
|
@@ -2582,6 +2594,24 @@ const functionParamsPerLine = {
|
|
|
2582
2594
|
// Check if this pattern has 2+ properties
|
|
2583
2595
|
if (pattern.properties.length >= 2) return true;
|
|
2584
2596
|
|
|
2597
|
+
// Check if type annotation has 2+ members (TSTypeLiteral)
|
|
2598
|
+
if (pattern.typeAnnotation && pattern.typeAnnotation.typeAnnotation) {
|
|
2599
|
+
const typeAnnotation = pattern.typeAnnotation.typeAnnotation;
|
|
2600
|
+
|
|
2601
|
+
if (typeAnnotation.type === "TSTypeLiteral" && typeAnnotation.members.length >= 2) {
|
|
2602
|
+
return true;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// Also check intersection types for TSTypeLiteral with 2+ members
|
|
2606
|
+
if (typeAnnotation.type === "TSIntersectionType") {
|
|
2607
|
+
const typeLiteral = typeAnnotation.types.find((t) => t.type === "TSTypeLiteral");
|
|
2608
|
+
|
|
2609
|
+
if (typeLiteral && typeLiteral.members.length >= 2) {
|
|
2610
|
+
return true;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2585
2615
|
// Check nested patterns
|
|
2586
2616
|
for (const prop of pattern.properties) {
|
|
2587
2617
|
if (prop.type === "Property" && prop.value) {
|
|
@@ -5304,9 +5334,35 @@ const logicalExpressionMultiline = {
|
|
|
5304
5334
|
// Collect all operands
|
|
5305
5335
|
const operands = collectOperandsHandler(node);
|
|
5306
5336
|
|
|
5307
|
-
//
|
|
5308
|
-
if (operands.length <= maxOperands)
|
|
5337
|
+
// Case 1: Simple expression (≤maxOperands) that's multiline → collapse to single line
|
|
5338
|
+
if (operands.length <= maxOperands) {
|
|
5339
|
+
if (isMultilineHandler(node)) {
|
|
5340
|
+
// Skip if any operand is itself multiline (e.g., JSX elements, function calls)
|
|
5341
|
+
const hasMultilineOperand = operands.some((op) => op.loc.start.line !== op.loc.end.line);
|
|
5342
|
+
|
|
5343
|
+
if (hasMultilineOperand) return;
|
|
5309
5344
|
|
|
5345
|
+
context.report({
|
|
5346
|
+
fix(fixer) {
|
|
5347
|
+
// Build single line: operand1 op operand2 op operand3
|
|
5348
|
+
const parts = [sourceCode.getText(operands[0])];
|
|
5349
|
+
|
|
5350
|
+
for (let i = 1; i < operands.length; i++) {
|
|
5351
|
+
const operator = getOperatorHandler(operands[i - 1], operands[i]);
|
|
5352
|
+
parts.push(` ${operator} ${sourceCode.getText(operands[i])}`);
|
|
5353
|
+
}
|
|
5354
|
+
|
|
5355
|
+
return fixer.replaceText(node, parts.join(""));
|
|
5356
|
+
},
|
|
5357
|
+
message: `Logical expression with ${operands.length} operands should be on a single line (max for multiline: ${maxOperands})`,
|
|
5358
|
+
node,
|
|
5359
|
+
});
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5362
|
+
return;
|
|
5363
|
+
}
|
|
5364
|
+
|
|
5365
|
+
// Case 2: Complex expression (>maxOperands) → enforce multiline
|
|
5310
5366
|
// Check if already properly multiline
|
|
5311
5367
|
if (isMultilineHandler(node)) {
|
|
5312
5368
|
// Check if each operand is on its own line
|
|
@@ -5361,7 +5417,7 @@ const logicalExpressionMultiline = {
|
|
|
5361
5417
|
};
|
|
5362
5418
|
},
|
|
5363
5419
|
meta: {
|
|
5364
|
-
docs: { description: "Enforce
|
|
5420
|
+
docs: { description: "Enforce single line for ≤maxOperands, multiline for >maxOperands logical expressions" },
|
|
5365
5421
|
fixable: "code",
|
|
5366
5422
|
schema: [
|
|
5367
5423
|
{
|
|
@@ -14069,6 +14125,10 @@ const noHardcodedStrings = {
|
|
|
14069
14125
|
"cellSpacing",
|
|
14070
14126
|
"charSet",
|
|
14071
14127
|
"className",
|
|
14128
|
+
"clipPath", // SVG
|
|
14129
|
+
"clipRule", // SVG
|
|
14130
|
+
"colorInterpolation", // SVG
|
|
14131
|
+
"colorInterpolationFilters", // SVG
|
|
14072
14132
|
"classNames",
|
|
14073
14133
|
"colSpan",
|
|
14074
14134
|
"contentEditable",
|
|
@@ -14090,7 +14150,12 @@ const noHardcodedStrings = {
|
|
|
14090
14150
|
"encType",
|
|
14091
14151
|
"enterKeyHint",
|
|
14092
14152
|
"fill", // SVG
|
|
14153
|
+
"fillOpacity", // SVG
|
|
14093
14154
|
"fillRule", // SVG
|
|
14155
|
+
"filter", // SVG filter reference
|
|
14156
|
+
"filterUnits", // SVG
|
|
14157
|
+
"floodColor", // SVG
|
|
14158
|
+
"floodOpacity", // SVG
|
|
14094
14159
|
"for",
|
|
14095
14160
|
"form",
|
|
14096
14161
|
"formAction",
|
|
@@ -14107,8 +14172,12 @@ const noHardcodedStrings = {
|
|
|
14107
14172
|
"hrefLang",
|
|
14108
14173
|
"htmlFor",
|
|
14109
14174
|
"httpEquiv",
|
|
14175
|
+
"gradientTransform", // SVG
|
|
14176
|
+
"gradientUnits", // SVG
|
|
14110
14177
|
"icon",
|
|
14111
14178
|
"id",
|
|
14179
|
+
"in", // SVG filter input
|
|
14180
|
+
"in2", // SVG filter input
|
|
14112
14181
|
"imagesizes",
|
|
14113
14182
|
"imagesrcset",
|
|
14114
14183
|
"inputMode",
|
|
@@ -14130,7 +14199,13 @@ const noHardcodedStrings = {
|
|
|
14130
14199
|
"low",
|
|
14131
14200
|
"marginHeight",
|
|
14132
14201
|
"marginWidth",
|
|
14202
|
+
"markerEnd", // SVG
|
|
14203
|
+
"markerMid", // SVG
|
|
14204
|
+
"markerStart", // SVG
|
|
14205
|
+
"markerUnits", // SVG
|
|
14206
|
+
"mask", // SVG
|
|
14133
14207
|
"max",
|
|
14208
|
+
"mode", // SVG blend mode
|
|
14134
14209
|
"maxLength",
|
|
14135
14210
|
"media",
|
|
14136
14211
|
"mediaGroup",
|
|
@@ -14146,7 +14221,11 @@ const noHardcodedStrings = {
|
|
|
14146
14221
|
"open",
|
|
14147
14222
|
"optimum",
|
|
14148
14223
|
"pattern",
|
|
14224
|
+
"patternContentUnits", // SVG
|
|
14225
|
+
"patternTransform", // SVG
|
|
14226
|
+
"patternUnits", // SVG
|
|
14149
14227
|
"ping",
|
|
14228
|
+
"preserveAspectRatio", // SVG
|
|
14150
14229
|
"playsInline",
|
|
14151
14230
|
"poster",
|
|
14152
14231
|
"preload",
|
|
@@ -14155,7 +14234,10 @@ const noHardcodedStrings = {
|
|
|
14155
14234
|
"readOnly",
|
|
14156
14235
|
"referrerPolicy",
|
|
14157
14236
|
"rel",
|
|
14237
|
+
"repeatCount", // SVG
|
|
14238
|
+
"repeatDur", // SVG
|
|
14158
14239
|
"required",
|
|
14240
|
+
"result", // SVG filter result
|
|
14159
14241
|
"reversed",
|
|
14160
14242
|
"role",
|
|
14161
14243
|
"rowSpan",
|
|
@@ -14177,23 +14259,86 @@ const noHardcodedStrings = {
|
|
|
14177
14259
|
"srcSet",
|
|
14178
14260
|
"start",
|
|
14179
14261
|
"step",
|
|
14262
|
+
"spreadMethod", // SVG
|
|
14263
|
+
"stdDeviation", // SVG filter blur
|
|
14264
|
+
"stopColor", // SVG gradient
|
|
14265
|
+
"stopOpacity", // SVG gradient
|
|
14180
14266
|
"stroke", // SVG
|
|
14267
|
+
"strokeDasharray", // SVG
|
|
14268
|
+
"strokeDashoffset", // SVG
|
|
14269
|
+
"strokeLinecap", // SVG
|
|
14270
|
+
"strokeLinejoin", // SVG
|
|
14271
|
+
"strokeMiterlimit", // SVG
|
|
14272
|
+
"strokeOpacity", // SVG
|
|
14181
14273
|
"strokeWidth", // SVG
|
|
14182
14274
|
"style",
|
|
14183
14275
|
"summary",
|
|
14184
14276
|
"tabIndex",
|
|
14185
14277
|
"target",
|
|
14186
14278
|
"testId",
|
|
14279
|
+
"textAnchor", // SVG
|
|
14280
|
+
"textDecoration", // SVG
|
|
14187
14281
|
"transform", // SVG
|
|
14188
14282
|
"translate",
|
|
14189
14283
|
"type",
|
|
14284
|
+
"vectorEffect", // SVG
|
|
14190
14285
|
"useMap",
|
|
14191
14286
|
"value",
|
|
14192
14287
|
"viewBox", // SVG
|
|
14193
14288
|
"width",
|
|
14194
14289
|
"wmode",
|
|
14195
14290
|
"wrap",
|
|
14291
|
+
"x", // SVG coordinate
|
|
14292
|
+
"x1", // SVG line coordinate
|
|
14293
|
+
"x2", // SVG line coordinate
|
|
14196
14294
|
"xmlns",
|
|
14295
|
+
"y", // SVG coordinate
|
|
14296
|
+
"y1", // SVG line coordinate
|
|
14297
|
+
"y2", // SVG line coordinate
|
|
14298
|
+
// SVG filter primitive attributes
|
|
14299
|
+
"baseFrequency",
|
|
14300
|
+
"numOctaves",
|
|
14301
|
+
"seed",
|
|
14302
|
+
"stitchTiles",
|
|
14303
|
+
"operator",
|
|
14304
|
+
"k1",
|
|
14305
|
+
"k2",
|
|
14306
|
+
"k3",
|
|
14307
|
+
"k4",
|
|
14308
|
+
"surfaceScale",
|
|
14309
|
+
"diffuseConstant",
|
|
14310
|
+
"specularConstant",
|
|
14311
|
+
"specularExponent",
|
|
14312
|
+
"kernelMatrix",
|
|
14313
|
+
"order",
|
|
14314
|
+
"targetX",
|
|
14315
|
+
"targetY",
|
|
14316
|
+
"edgeMode",
|
|
14317
|
+
"kernelUnitLength",
|
|
14318
|
+
"bias",
|
|
14319
|
+
"divisor",
|
|
14320
|
+
"preserveAlpha",
|
|
14321
|
+
"radius",
|
|
14322
|
+
"azimuth",
|
|
14323
|
+
"elevation",
|
|
14324
|
+
"limitingConeAngle",
|
|
14325
|
+
"pointsAtX",
|
|
14326
|
+
"pointsAtY",
|
|
14327
|
+
"pointsAtZ",
|
|
14328
|
+
// SVG shape attributes
|
|
14329
|
+
"cx", // circle/ellipse center x
|
|
14330
|
+
"cy", // circle/ellipse center y
|
|
14331
|
+
"r", // circle radius
|
|
14332
|
+
"rx", // ellipse radius x
|
|
14333
|
+
"ry", // ellipse radius y
|
|
14334
|
+
"points", // polygon/polyline points
|
|
14335
|
+
"pathLength",
|
|
14336
|
+
"offset", // gradient offset
|
|
14337
|
+
"dx", // text offset
|
|
14338
|
+
"dy", // text offset
|
|
14339
|
+
"rotate", // text rotate
|
|
14340
|
+
"lengthAdjust",
|
|
14341
|
+
"textLength",
|
|
14197
14342
|
];
|
|
14198
14343
|
|
|
14199
14344
|
const ignoreAttributes = options.ignoreAttributes
|
|
@@ -14207,9 +14352,27 @@ const noHardcodedStrings = {
|
|
|
14207
14352
|
/^.$/,
|
|
14208
14353
|
// CSS units and values
|
|
14209
14354
|
/^-?\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc|deg|rad|turn|s|ms|fr)?$/,
|
|
14355
|
+
// Scientific notation numbers (common in SVG coordinates)
|
|
14356
|
+
/^-?\d+(\.\d+)?e[+-]?\d+$/i,
|
|
14210
14357
|
// Colors (hex, rgb, hsl)
|
|
14211
14358
|
/^#[0-9a-fA-F]{3,8}$/,
|
|
14212
14359
|
/^(rgb|rgba|hsl|hsla)\(.+\)$/,
|
|
14360
|
+
// URL references (SVG filters, clips, etc.)
|
|
14361
|
+
/^url\(#?.+\)$/,
|
|
14362
|
+
// SVG standard attribute values
|
|
14363
|
+
/^(round|butt|square|miter|bevel|none|normal|evenodd|nonzero|sRGB|linearRGB|userSpaceOnUse|objectBoundingBox|pad|reflect|repeat|auto|inherit|currentColor|meet|slice|xMinYMin|xMidYMin|xMaxYMin|xMinYMid|xMidYMid|xMaxYMid|xMinYMax|xMidYMax|xMaxYMax|stitch|noStitch|duplicate|wrap|arithmetic|atop|in|out|over|xor|dilate|erode|matrix|saturate|hueRotate|luminanceToAlpha|discrete|linear|gamma|table|identity|SourceGraphic|SourceAlpha|BackgroundImage|BackgroundAlpha|FillPaint|StrokePaint)$/,
|
|
14364
|
+
// SVG filter result/internal identifiers (patterns like effect1_foregroundBlur, filter0_f_21_211, BackgroundImageFix)
|
|
14365
|
+
/^[a-zA-Z]+\d*[_a-zA-Z0-9]*(_[a-zA-Z0-9]+)+$/,
|
|
14366
|
+
// Color names (CSS named colors used in SVG)
|
|
14367
|
+
/^(white|black|red|green|blue|yellow|orange|purple|pink|brown|gray|grey|cyan|magenta|transparent)$/i,
|
|
14368
|
+
// CSS cursor values
|
|
14369
|
+
/^(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|text|vertical-text|alias|copy|move|no-drop|not-allowed|grab|grabbing|all-scroll|col-resize|row-resize|n-resize|e-resize|s-resize|w-resize|ne-resize|nw-resize|se-resize|sw-resize|ew-resize|ns-resize|nesw-resize|nwse-resize|zoom-in|zoom-out)$/,
|
|
14370
|
+
// CSS display/visibility values
|
|
14371
|
+
/^(block|inline|inline-block|flex|inline-flex|grid|inline-grid|flow-root|contents|table|table-row|table-cell|list-item|none|visible|hidden|collapse)$/,
|
|
14372
|
+
// CSS position values
|
|
14373
|
+
/^(static|relative|absolute|fixed|sticky)$/,
|
|
14374
|
+
// CSS overflow values
|
|
14375
|
+
/^(visible|hidden|scroll|auto|clip)$/,
|
|
14213
14376
|
// URLs and paths
|
|
14214
14377
|
/^(https?:\/\/|\/\/|\/|\.\/|\.\.\/)/,
|
|
14215
14378
|
// Data URLs
|
|
@@ -14766,6 +14929,18 @@ const noHardcodedStrings = {
|
|
|
14766
14929
|
// Skip if it's a reference to an imported constant
|
|
14767
14930
|
if (isImportedConstantHandler(expression)) return;
|
|
14768
14931
|
|
|
14932
|
+
// Check if we're inside a JSX attribute that should be ignored (like className)
|
|
14933
|
+
if (node.parent && node.parent.type === "JSXAttribute") {
|
|
14934
|
+
const attrName = node.parent.name.name
|
|
14935
|
+
|| (node.parent.name.namespace && `${node.parent.name.namespace.name}:${node.parent.name.name.name}`);
|
|
14936
|
+
|
|
14937
|
+
// Skip if attribute is in ignore list (className, style, etc.)
|
|
14938
|
+
if (ignoreAttributes.includes(attrName)) return;
|
|
14939
|
+
|
|
14940
|
+
// Skip data-* and aria-* attributes
|
|
14941
|
+
if (attrName && (attrName.startsWith("data-") || attrName.startsWith("aria-"))) return;
|
|
14942
|
+
}
|
|
14943
|
+
|
|
14769
14944
|
// Check string literals
|
|
14770
14945
|
if (expression.type === "Literal" && typeof expression.value === "string") {
|
|
14771
14946
|
const str = expression.value;
|
|
@@ -16961,8 +17136,8 @@ const componentPropsInlineType = {
|
|
|
16961
17136
|
}
|
|
16962
17137
|
});
|
|
16963
17138
|
|
|
16964
|
-
// Check that last member has trailing comma
|
|
16965
|
-
if (members.length >
|
|
17139
|
+
// Check that last member has trailing comma (only for multiple members)
|
|
17140
|
+
if (members.length > 1) {
|
|
16966
17141
|
const lastMember = members[members.length - 1];
|
|
16967
17142
|
const lastMemberText = sourceCode.getText(lastMember);
|
|
16968
17143
|
|
|
@@ -16975,6 +17150,25 @@ const componentPropsInlineType = {
|
|
|
16975
17150
|
}
|
|
16976
17151
|
}
|
|
16977
17152
|
|
|
17153
|
+
// Remove trailing comma for single member on single line
|
|
17154
|
+
if (members.length === 1) {
|
|
17155
|
+
const member = members[0];
|
|
17156
|
+
const memberText = sourceCode.getText(member);
|
|
17157
|
+
|
|
17158
|
+
if (memberText.trimEnd().endsWith(",")) {
|
|
17159
|
+
context.report({
|
|
17160
|
+
fix: (fixer) => {
|
|
17161
|
+
const lastCommaIndex = memberText.lastIndexOf(",");
|
|
17162
|
+
const absolutePos = member.range[0] + lastCommaIndex;
|
|
17163
|
+
|
|
17164
|
+
return fixer.removeRange([absolutePos, absolutePos + 1]);
|
|
17165
|
+
},
|
|
17166
|
+
message: "Single props type property should not have trailing comma",
|
|
17167
|
+
node: member,
|
|
17168
|
+
});
|
|
17169
|
+
}
|
|
17170
|
+
}
|
|
17171
|
+
|
|
16978
17172
|
// Check for empty lines before closing brace
|
|
16979
17173
|
if (members.length > 0 && closeBraceToken) {
|
|
16980
17174
|
const lastMember = members[members.length - 1];
|
|
@@ -17110,6 +17304,22 @@ const componentPropsInlineType = {
|
|
|
17110
17304
|
}
|
|
17111
17305
|
}
|
|
17112
17306
|
|
|
17307
|
+
// Check closing brace position - should be on its own line for multiple members
|
|
17308
|
+
if (members.length > 1 && closeBraceToken) {
|
|
17309
|
+
const lastMember = members[members.length - 1];
|
|
17310
|
+
|
|
17311
|
+
if (closeBraceToken.loc.start.line === lastMember.loc.end.line) {
|
|
17312
|
+
context.report({
|
|
17313
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
17314
|
+
[lastMember.range[1], closeBraceToken.range[0]],
|
|
17315
|
+
"\n" + baseIndent,
|
|
17316
|
+
),
|
|
17317
|
+
message: "Closing brace must be on its own line when there are multiple properties",
|
|
17318
|
+
node: closeBraceToken,
|
|
17319
|
+
});
|
|
17320
|
+
}
|
|
17321
|
+
}
|
|
17322
|
+
|
|
17113
17323
|
// Check each member for semicolons vs commas and line formatting
|
|
17114
17324
|
members.forEach((member, index) => {
|
|
17115
17325
|
const memberText = sourceCode.getText(member);
|
|
@@ -17198,8 +17408,8 @@ const componentPropsInlineType = {
|
|
|
17198
17408
|
}
|
|
17199
17409
|
});
|
|
17200
17410
|
|
|
17201
|
-
// Check that last member has trailing comma
|
|
17202
|
-
if (members.length >
|
|
17411
|
+
// Check that last member has trailing comma (only for multiple members)
|
|
17412
|
+
if (members.length > 1) {
|
|
17203
17413
|
const lastMember = members[members.length - 1];
|
|
17204
17414
|
const lastMemberText = sourceCode.getText(lastMember);
|
|
17205
17415
|
|
|
@@ -17211,6 +17421,25 @@ const componentPropsInlineType = {
|
|
|
17211
17421
|
});
|
|
17212
17422
|
}
|
|
17213
17423
|
}
|
|
17424
|
+
|
|
17425
|
+
// Remove trailing comma for single member on single line
|
|
17426
|
+
if (members.length === 1) {
|
|
17427
|
+
const member = members[0];
|
|
17428
|
+
const memberText = sourceCode.getText(member);
|
|
17429
|
+
|
|
17430
|
+
if (memberText.trimEnd().endsWith(",")) {
|
|
17431
|
+
context.report({
|
|
17432
|
+
fix: (fixer) => {
|
|
17433
|
+
const lastCommaIndex = memberText.lastIndexOf(",");
|
|
17434
|
+
const absolutePos = member.range[0] + lastCommaIndex;
|
|
17435
|
+
|
|
17436
|
+
return fixer.removeRange([absolutePos, absolutePos + 1]);
|
|
17437
|
+
},
|
|
17438
|
+
message: "Single props type property should not have trailing comma",
|
|
17439
|
+
node: member,
|
|
17440
|
+
});
|
|
17441
|
+
}
|
|
17442
|
+
}
|
|
17214
17443
|
}
|
|
17215
17444
|
};
|
|
17216
17445
|
|
|
@@ -17305,6 +17534,140 @@ const componentPropsInlineType = {
|
|
|
17305
17534
|
},
|
|
17306
17535
|
};
|
|
17307
17536
|
|
|
17537
|
+
/**
|
|
17538
|
+
* ───────────────────────────────────────────────────────────────
|
|
17539
|
+
* Rule: SVG Component Icon Naming
|
|
17540
|
+
* ───────────────────────────────────────────────────────────────
|
|
17541
|
+
*
|
|
17542
|
+
* Description:
|
|
17543
|
+
* Components that return only an SVG element must have a name
|
|
17544
|
+
* ending with "Icon". Conversely, components with "Icon" suffix
|
|
17545
|
+
* must return an SVG element.
|
|
17546
|
+
*
|
|
17547
|
+
* ✓ Good:
|
|
17548
|
+
* export const SuccessIcon = ({ className }: { className?: string }) => (
|
|
17549
|
+
* <svg className={className}>...</svg>
|
|
17550
|
+
* );
|
|
17551
|
+
*
|
|
17552
|
+
* export const Button = ({ children }: { children: ReactNode }) => (
|
|
17553
|
+
* <button>{children}</button>
|
|
17554
|
+
* );
|
|
17555
|
+
*
|
|
17556
|
+
* ✗ Bad:
|
|
17557
|
+
* // Returns SVG but doesn't end with "Icon"
|
|
17558
|
+
* export const Success = ({ className }: { className?: string }) => (
|
|
17559
|
+
* <svg className={className}>...</svg>
|
|
17560
|
+
* );
|
|
17561
|
+
*
|
|
17562
|
+
* // Ends with "Icon" but doesn't return SVG
|
|
17563
|
+
* export const ButtonIcon = ({ children }: { children: ReactNode }) => (
|
|
17564
|
+
* <button>{children}</button>
|
|
17565
|
+
* );
|
|
17566
|
+
*/
|
|
17567
|
+
const svgComponentIconNaming = {
|
|
17568
|
+
create(context) {
|
|
17569
|
+
// Get the component name from node
|
|
17570
|
+
const getComponentNameHandler = (node) => {
|
|
17571
|
+
// Arrow function: const Name = () => ...
|
|
17572
|
+
if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id && node.parent.id.type === "Identifier") {
|
|
17573
|
+
return node.parent.id.name;
|
|
17574
|
+
}
|
|
17575
|
+
|
|
17576
|
+
// Function declaration: function Name() { ... }
|
|
17577
|
+
if (node.id && node.id.type === "Identifier") {
|
|
17578
|
+
return node.id.name;
|
|
17579
|
+
}
|
|
17580
|
+
|
|
17581
|
+
return null;
|
|
17582
|
+
};
|
|
17583
|
+
|
|
17584
|
+
// Check if component name starts with uppercase (React component convention)
|
|
17585
|
+
const isReactComponentNameHandler = (name) => name && /^[A-Z]/.test(name);
|
|
17586
|
+
|
|
17587
|
+
// Check if name ends with "Icon"
|
|
17588
|
+
const hasIconSuffixHandler = (name) => name && name.endsWith("Icon");
|
|
17589
|
+
|
|
17590
|
+
// Check if the return value is purely an SVG element
|
|
17591
|
+
const returnsSvgOnlyHandler = (node) => {
|
|
17592
|
+
const body = node.body;
|
|
17593
|
+
|
|
17594
|
+
if (!body) return false;
|
|
17595
|
+
|
|
17596
|
+
// Arrow function with expression body: () => <svg>...</svg>
|
|
17597
|
+
if (body.type === "JSXElement") {
|
|
17598
|
+
return body.openingElement && body.openingElement.name && body.openingElement.name.name === "svg";
|
|
17599
|
+
}
|
|
17600
|
+
|
|
17601
|
+
// Arrow function with parenthesized expression: () => (<svg>...</svg>)
|
|
17602
|
+
if (body.type === "ParenthesizedExpression" && body.expression) {
|
|
17603
|
+
if (body.expression.type === "JSXElement") {
|
|
17604
|
+
return body.expression.openingElement && body.expression.openingElement.name && body.expression.openingElement.name.name === "svg";
|
|
17605
|
+
}
|
|
17606
|
+
}
|
|
17607
|
+
|
|
17608
|
+
// Block body with return statement: () => { return <svg>...</svg>; }
|
|
17609
|
+
if (body.type === "BlockStatement") {
|
|
17610
|
+
// Find all return statements
|
|
17611
|
+
const returnStatements = body.body.filter((stmt) => stmt.type === "ReturnStatement" && stmt.argument);
|
|
17612
|
+
|
|
17613
|
+
// Should have exactly one return statement for a simple SVG component
|
|
17614
|
+
if (returnStatements.length === 1) {
|
|
17615
|
+
const returnArg = returnStatements[0].argument;
|
|
17616
|
+
|
|
17617
|
+
if (returnArg.type === "JSXElement") {
|
|
17618
|
+
return returnArg.openingElement && returnArg.openingElement.name && returnArg.openingElement.name.name === "svg";
|
|
17619
|
+
}
|
|
17620
|
+
|
|
17621
|
+
// Parenthesized: return (<svg>...</svg>);
|
|
17622
|
+
if (returnArg.type === "ParenthesizedExpression" && returnArg.expression && returnArg.expression.type === "JSXElement") {
|
|
17623
|
+
return returnArg.expression.openingElement && returnArg.expression.openingElement.name && returnArg.expression.openingElement.name.name === "svg";
|
|
17624
|
+
}
|
|
17625
|
+
}
|
|
17626
|
+
}
|
|
17627
|
+
|
|
17628
|
+
return false;
|
|
17629
|
+
};
|
|
17630
|
+
|
|
17631
|
+
const checkFunctionHandler = (node) => {
|
|
17632
|
+
const componentName = getComponentNameHandler(node);
|
|
17633
|
+
|
|
17634
|
+
// Only check React components (PascalCase)
|
|
17635
|
+
if (!isReactComponentNameHandler(componentName)) return;
|
|
17636
|
+
|
|
17637
|
+
const returnsSvg = returnsSvgOnlyHandler(node);
|
|
17638
|
+
const hasIconSuffix = hasIconSuffixHandler(componentName);
|
|
17639
|
+
|
|
17640
|
+
// Case 1: Returns SVG but doesn't end with "Icon"
|
|
17641
|
+
if (returnsSvg && !hasIconSuffix) {
|
|
17642
|
+
context.report({
|
|
17643
|
+
message: `Component "${componentName}" returns an SVG element and should end with "Icon" suffix (e.g., "${componentName}Icon")`,
|
|
17644
|
+
node: node.parent && node.parent.type === "VariableDeclarator" ? node.parent.id : node.id || node,
|
|
17645
|
+
});
|
|
17646
|
+
}
|
|
17647
|
+
|
|
17648
|
+
// Case 2: Ends with "Icon" but doesn't return SVG
|
|
17649
|
+
if (hasIconSuffix && !returnsSvg) {
|
|
17650
|
+
context.report({
|
|
17651
|
+
message: `Component "${componentName}" has "Icon" suffix but doesn't return an SVG element. Either rename it or make it return an SVG.`,
|
|
17652
|
+
node: node.parent && node.parent.type === "VariableDeclarator" ? node.parent.id : node.id || node,
|
|
17653
|
+
});
|
|
17654
|
+
}
|
|
17655
|
+
};
|
|
17656
|
+
|
|
17657
|
+
return {
|
|
17658
|
+
ArrowFunctionExpression: checkFunctionHandler,
|
|
17659
|
+
FunctionDeclaration: checkFunctionHandler,
|
|
17660
|
+
FunctionExpression: checkFunctionHandler,
|
|
17661
|
+
};
|
|
17662
|
+
},
|
|
17663
|
+
meta: {
|
|
17664
|
+
docs: { description: "Enforce SVG components to have 'Icon' suffix and vice versa" },
|
|
17665
|
+
fixable: null,
|
|
17666
|
+
schema: [],
|
|
17667
|
+
type: "suggestion",
|
|
17668
|
+
},
|
|
17669
|
+
};
|
|
17670
|
+
|
|
17308
17671
|
/**
|
|
17309
17672
|
* ───────────────────────────────────────────────────────────────
|
|
17310
17673
|
* Rule: No Inline Type Definitions
|
|
@@ -20082,6 +20445,7 @@ export default {
|
|
|
20082
20445
|
// Component rules
|
|
20083
20446
|
"component-props-destructure": componentPropsDestructure,
|
|
20084
20447
|
"component-props-inline-type": componentPropsInlineType,
|
|
20448
|
+
"svg-component-icon-naming": svgComponentIconNaming,
|
|
20085
20449
|
|
|
20086
20450
|
// React rules
|
|
20087
20451
|
"react-code-order": reactCodeOrder,
|
package/package.json
CHANGED