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 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.10.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.0...v1.10.0
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
- *71 rules (64 auto-fixable) to keep your codebase clean and consistent*
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 **71 custom rules** (64 auto-fixable) for code formatting. Built for **ESLint v9 flat configs**.
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** — 64 of 71 rules support auto-fix with `eslint --fix`
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
- **64 rules** support automatic fixing with `eslint --fix`. 6 rules are report-only (require manual changes).
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
- > **71 rules total** — 64 with auto-fix 🔧, 7 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
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
- 64 of 71 rules support auto-fixing. Run ESLint with the `--fix` flag:
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
- return isShorthand ? key : `${key}: ${value}`;
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
- return `{ ${props.join(", ")} }`;
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
- // If operands count is within threshold, skip
5308
- if (operands.length <= maxOperands) return;
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 multiline formatting for logical expressions with more than maxOperands" },
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 > 0) {
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 > 0) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "A custom ESLint plugin for enforcing consistent code formatting and style rules in React/JSX projects",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",