eslint-plugin-primer-react 8.3.0-rc.4b58a28 → 8.4.0-rc.6ac270a
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/.github/workflows/check-for-changeset.yml +1 -1
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/release_canary.yml +1 -1
- package/.github/workflows/release_candidate.yml +1 -1
- package/CHANGELOG.md +6 -0
- package/docs/rules/spread-props-first.md +66 -0
- package/package-lock.json +58 -55
- package/package.json +1 -1
- package/src/index.js +1 -0
- package/src/rules/__tests__/spread-props-first.test.js +123 -0
- package/src/rules/spread-props-first.js +81 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
format:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
12
|
+
- uses: actions/checkout@v5
|
|
13
13
|
- name: Use Node.js
|
|
14
14
|
uses: actions/setup-node@v4
|
|
15
15
|
with:
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
test:
|
|
22
22
|
runs-on: ubuntu-latest
|
|
23
23
|
steps:
|
|
24
|
-
- uses: actions/checkout@
|
|
24
|
+
- uses: actions/checkout@v5
|
|
25
25
|
- name: Use Node.js
|
|
26
26
|
uses: actions/setup-node@v4
|
|
27
27
|
with:
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
lint:
|
|
34
34
|
runs-on: ubuntu-latest
|
|
35
35
|
steps:
|
|
36
|
-
- uses: actions/checkout@
|
|
36
|
+
- uses: actions/checkout@v5
|
|
37
37
|
- name: Use Node.js
|
|
38
38
|
uses: actions/setup-node@v4
|
|
39
39
|
with:
|
|
@@ -8,7 +8,7 @@ jobs:
|
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
9
|
steps:
|
|
10
10
|
- name: Checkout repository
|
|
11
|
-
uses: actions/checkout@
|
|
11
|
+
uses: actions/checkout@v5
|
|
12
12
|
with:
|
|
13
13
|
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
|
14
14
|
fetch-depth: 0
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
steps:
|
|
17
17
|
- name: Checkout repository
|
|
18
|
-
uses: actions/checkout@
|
|
18
|
+
uses: actions/checkout@v5
|
|
19
19
|
with:
|
|
20
20
|
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
|
21
21
|
fetch-depth: 0
|
|
@@ -11,7 +11,7 @@ jobs:
|
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
13
|
- name: Checkout repository
|
|
14
|
-
uses: actions/checkout@
|
|
14
|
+
uses: actions/checkout@v5
|
|
15
15
|
with:
|
|
16
16
|
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
|
17
17
|
fetch-depth: 0
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# eslint-plugin-primer-react
|
|
2
2
|
|
|
3
|
+
## 8.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#437](https://github.com/primer/eslint-plugin-primer-react/pull/437) [`9270d40`](https://github.com/primer/eslint-plugin-primer-react/commit/9270d40d73bd046e21156b68ef6bd13a20008585) Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Add spread-props-first rule to ensure spread props come before other props
|
|
8
|
+
|
|
3
9
|
## 8.3.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Ensure spread props come before other props (spread-props-first)
|
|
2
|
+
|
|
3
|
+
Spread props should come before other named props to avoid unintentionally overriding props. When spread props are placed after named props, they can override the named props, which is often unintended and can lead to UI bugs.
|
|
4
|
+
|
|
5
|
+
## Rule details
|
|
6
|
+
|
|
7
|
+
This rule enforces that all spread props (`{...rest}`, `{...props}`, etc.) come before any named props in JSX elements.
|
|
8
|
+
|
|
9
|
+
👎 Examples of **incorrect** code for this rule:
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
/* eslint primer-react/spread-props-first: "error" */
|
|
13
|
+
|
|
14
|
+
// ❌ Spread after named prop
|
|
15
|
+
<Example className="..." {...rest} />
|
|
16
|
+
|
|
17
|
+
// ❌ Spread in the middle
|
|
18
|
+
<Example className="..." {...rest} id="foo" />
|
|
19
|
+
|
|
20
|
+
// ❌ Multiple spreads after named props
|
|
21
|
+
<Example className="..." {...rest} {...other} />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
👍 Examples of **correct** code for this rule:
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
/* eslint primer-react/spread-props-first: "error" */
|
|
28
|
+
|
|
29
|
+
// ✅ Spread before named props
|
|
30
|
+
<Example {...rest} className="..." />
|
|
31
|
+
|
|
32
|
+
// ✅ Multiple spreads before named props
|
|
33
|
+
<Example {...rest} {...other} className="..." />
|
|
34
|
+
|
|
35
|
+
// ✅ Only spread props
|
|
36
|
+
<Example {...rest} />
|
|
37
|
+
|
|
38
|
+
// ✅ Only named props
|
|
39
|
+
<Example className="..." id="foo" />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Why this matters
|
|
43
|
+
|
|
44
|
+
Placing spread props after named props can cause unexpected behavior:
|
|
45
|
+
|
|
46
|
+
```jsx
|
|
47
|
+
// ❌ Bad: className might get overridden by rest
|
|
48
|
+
<Button className="custom-class" {...rest} />
|
|
49
|
+
|
|
50
|
+
// If rest = { className: "other-class" }
|
|
51
|
+
// Result: className="other-class" (custom-class is lost!)
|
|
52
|
+
|
|
53
|
+
// ✅ Good: className will override any className in rest
|
|
54
|
+
<Button {...rest} className="custom-class" />
|
|
55
|
+
|
|
56
|
+
// If rest = { className: "other-class" }
|
|
57
|
+
// Result: className="custom-class" (as intended)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
This rule has no configuration options.
|
|
63
|
+
|
|
64
|
+
## When to use autofix
|
|
65
|
+
|
|
66
|
+
This rule includes an autofix that will automatically reorder your props to place all spread props first. The autofix is safe to use as it preserves the order of spreads relative to each other and the order of named props relative to each other.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-primer-react",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "eslint-plugin-primer-react",
|
|
9
|
-
"version": "8.
|
|
9
|
+
"version": "8.3.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@styled-system/props": "^5.1.5",
|
|
@@ -956,11 +956,12 @@
|
|
|
956
956
|
}
|
|
957
957
|
},
|
|
958
958
|
"node_modules/@eslint/config-array": {
|
|
959
|
-
"version": "0.21.
|
|
960
|
-
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.
|
|
961
|
-
"integrity": "sha512-
|
|
959
|
+
"version": "0.21.1",
|
|
960
|
+
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
|
961
|
+
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
|
962
|
+
"license": "Apache-2.0",
|
|
962
963
|
"dependencies": {
|
|
963
|
-
"@eslint/object-schema": "^2.1.
|
|
964
|
+
"@eslint/object-schema": "^2.1.7",
|
|
964
965
|
"debug": "^4.3.1",
|
|
965
966
|
"minimatch": "^3.1.2"
|
|
966
967
|
},
|
|
@@ -972,6 +973,7 @@
|
|
|
972
973
|
"version": "1.1.12",
|
|
973
974
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
|
974
975
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
|
976
|
+
"license": "MIT",
|
|
975
977
|
"dependencies": {
|
|
976
978
|
"balanced-match": "^1.0.0",
|
|
977
979
|
"concat-map": "0.0.1"
|
|
@@ -981,6 +983,7 @@
|
|
|
981
983
|
"version": "3.1.2",
|
|
982
984
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
983
985
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
|
986
|
+
"license": "ISC",
|
|
984
987
|
"dependencies": {
|
|
985
988
|
"brace-expansion": "^1.1.7"
|
|
986
989
|
},
|
|
@@ -989,9 +992,9 @@
|
|
|
989
992
|
}
|
|
990
993
|
},
|
|
991
994
|
"node_modules/@eslint/config-helpers": {
|
|
992
|
-
"version": "0.4.
|
|
993
|
-
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.
|
|
994
|
-
"integrity": "sha512-
|
|
995
|
+
"version": "0.4.1",
|
|
996
|
+
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
|
|
997
|
+
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
|
|
995
998
|
"license": "Apache-2.0",
|
|
996
999
|
"dependencies": {
|
|
997
1000
|
"@eslint/core": "^0.16.0"
|
|
@@ -1082,9 +1085,9 @@
|
|
|
1082
1085
|
}
|
|
1083
1086
|
},
|
|
1084
1087
|
"node_modules/@eslint/js": {
|
|
1085
|
-
"version": "9.
|
|
1086
|
-
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.
|
|
1087
|
-
"integrity": "sha512-
|
|
1088
|
+
"version": "9.38.0",
|
|
1089
|
+
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
|
|
1090
|
+
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
|
|
1088
1091
|
"license": "MIT",
|
|
1089
1092
|
"engines": {
|
|
1090
1093
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
@@ -1094,9 +1097,10 @@
|
|
|
1094
1097
|
}
|
|
1095
1098
|
},
|
|
1096
1099
|
"node_modules/@eslint/object-schema": {
|
|
1097
|
-
"version": "2.1.
|
|
1098
|
-
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.
|
|
1099
|
-
"integrity": "sha512-
|
|
1100
|
+
"version": "2.1.7",
|
|
1101
|
+
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
|
1102
|
+
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
|
1103
|
+
"license": "Apache-2.0",
|
|
1100
1104
|
"engines": {
|
|
1101
1105
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
1102
1106
|
}
|
|
@@ -2482,15 +2486,15 @@
|
|
|
2482
2486
|
}
|
|
2483
2487
|
},
|
|
2484
2488
|
"node_modules/@typescript-eslint/utils": {
|
|
2485
|
-
"version": "8.
|
|
2486
|
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.
|
|
2487
|
-
"integrity": "sha512-
|
|
2489
|
+
"version": "8.46.2",
|
|
2490
|
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
|
|
2491
|
+
"integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
|
|
2488
2492
|
"license": "MIT",
|
|
2489
2493
|
"dependencies": {
|
|
2490
2494
|
"@eslint-community/eslint-utils": "^4.7.0",
|
|
2491
|
-
"@typescript-eslint/scope-manager": "8.
|
|
2492
|
-
"@typescript-eslint/types": "8.
|
|
2493
|
-
"@typescript-eslint/typescript-estree": "8.
|
|
2495
|
+
"@typescript-eslint/scope-manager": "8.46.2",
|
|
2496
|
+
"@typescript-eslint/types": "8.46.2",
|
|
2497
|
+
"@typescript-eslint/typescript-estree": "8.46.2"
|
|
2494
2498
|
},
|
|
2495
2499
|
"engines": {
|
|
2496
2500
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
@@ -2505,13 +2509,13 @@
|
|
|
2505
2509
|
}
|
|
2506
2510
|
},
|
|
2507
2511
|
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": {
|
|
2508
|
-
"version": "8.
|
|
2509
|
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.
|
|
2510
|
-
"integrity": "sha512-
|
|
2512
|
+
"version": "8.46.2",
|
|
2513
|
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
|
|
2514
|
+
"integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
|
|
2511
2515
|
"license": "MIT",
|
|
2512
2516
|
"dependencies": {
|
|
2513
|
-
"@typescript-eslint/tsconfig-utils": "^8.
|
|
2514
|
-
"@typescript-eslint/types": "^8.
|
|
2517
|
+
"@typescript-eslint/tsconfig-utils": "^8.46.2",
|
|
2518
|
+
"@typescript-eslint/types": "^8.46.2",
|
|
2515
2519
|
"debug": "^4.3.4"
|
|
2516
2520
|
},
|
|
2517
2521
|
"engines": {
|
|
@@ -2526,13 +2530,13 @@
|
|
|
2526
2530
|
}
|
|
2527
2531
|
},
|
|
2528
2532
|
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
|
|
2529
|
-
"version": "8.
|
|
2530
|
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.
|
|
2531
|
-
"integrity": "sha512-
|
|
2533
|
+
"version": "8.46.2",
|
|
2534
|
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
|
|
2535
|
+
"integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
|
|
2532
2536
|
"license": "MIT",
|
|
2533
2537
|
"dependencies": {
|
|
2534
|
-
"@typescript-eslint/types": "8.
|
|
2535
|
-
"@typescript-eslint/visitor-keys": "8.
|
|
2538
|
+
"@typescript-eslint/types": "8.46.2",
|
|
2539
|
+
"@typescript-eslint/visitor-keys": "8.46.2"
|
|
2536
2540
|
},
|
|
2537
2541
|
"engines": {
|
|
2538
2542
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
@@ -2543,9 +2547,9 @@
|
|
|
2543
2547
|
}
|
|
2544
2548
|
},
|
|
2545
2549
|
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": {
|
|
2546
|
-
"version": "8.
|
|
2547
|
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.
|
|
2548
|
-
"integrity": "sha512-
|
|
2550
|
+
"version": "8.46.2",
|
|
2551
|
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
|
|
2552
|
+
"integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
|
|
2549
2553
|
"license": "MIT",
|
|
2550
2554
|
"engines": {
|
|
2551
2555
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
@@ -2559,9 +2563,9 @@
|
|
|
2559
2563
|
}
|
|
2560
2564
|
},
|
|
2561
2565
|
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
|
|
2562
|
-
"version": "8.
|
|
2563
|
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.
|
|
2564
|
-
"integrity": "sha512-
|
|
2566
|
+
"version": "8.46.2",
|
|
2567
|
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
|
|
2568
|
+
"integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
|
|
2565
2569
|
"license": "MIT",
|
|
2566
2570
|
"engines": {
|
|
2567
2571
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
@@ -2572,15 +2576,15 @@
|
|
|
2572
2576
|
}
|
|
2573
2577
|
},
|
|
2574
2578
|
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
|
|
2575
|
-
"version": "8.
|
|
2576
|
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.
|
|
2577
|
-
"integrity": "sha512-
|
|
2579
|
+
"version": "8.46.2",
|
|
2580
|
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
|
|
2581
|
+
"integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
|
|
2578
2582
|
"license": "MIT",
|
|
2579
2583
|
"dependencies": {
|
|
2580
|
-
"@typescript-eslint/project-service": "8.
|
|
2581
|
-
"@typescript-eslint/tsconfig-utils": "8.
|
|
2582
|
-
"@typescript-eslint/types": "8.
|
|
2583
|
-
"@typescript-eslint/visitor-keys": "8.
|
|
2584
|
+
"@typescript-eslint/project-service": "8.46.2",
|
|
2585
|
+
"@typescript-eslint/tsconfig-utils": "8.46.2",
|
|
2586
|
+
"@typescript-eslint/types": "8.46.2",
|
|
2587
|
+
"@typescript-eslint/visitor-keys": "8.46.2",
|
|
2584
2588
|
"debug": "^4.3.4",
|
|
2585
2589
|
"fast-glob": "^3.3.2",
|
|
2586
2590
|
"is-glob": "^4.0.3",
|
|
@@ -2600,12 +2604,12 @@
|
|
|
2600
2604
|
}
|
|
2601
2605
|
},
|
|
2602
2606
|
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
|
|
2603
|
-
"version": "8.
|
|
2604
|
-
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.
|
|
2605
|
-
"integrity": "sha512-
|
|
2607
|
+
"version": "8.46.2",
|
|
2608
|
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
|
|
2609
|
+
"integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
|
|
2606
2610
|
"license": "MIT",
|
|
2607
2611
|
"dependencies": {
|
|
2608
|
-
"@typescript-eslint/types": "8.
|
|
2612
|
+
"@typescript-eslint/types": "8.46.2",
|
|
2609
2613
|
"eslint-visitor-keys": "^4.2.1"
|
|
2610
2614
|
},
|
|
2611
2615
|
"engines": {
|
|
@@ -4148,24 +4152,23 @@
|
|
|
4148
4152
|
}
|
|
4149
4153
|
},
|
|
4150
4154
|
"node_modules/eslint": {
|
|
4151
|
-
"version": "9.
|
|
4152
|
-
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.
|
|
4153
|
-
"integrity": "sha512-
|
|
4155
|
+
"version": "9.38.0",
|
|
4156
|
+
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
|
|
4157
|
+
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
|
4154
4158
|
"license": "MIT",
|
|
4155
4159
|
"dependencies": {
|
|
4156
4160
|
"@eslint-community/eslint-utils": "^4.8.0",
|
|
4157
4161
|
"@eslint-community/regexpp": "^4.12.1",
|
|
4158
|
-
"@eslint/config-array": "^0.21.
|
|
4159
|
-
"@eslint/config-helpers": "^0.4.
|
|
4162
|
+
"@eslint/config-array": "^0.21.1",
|
|
4163
|
+
"@eslint/config-helpers": "^0.4.1",
|
|
4160
4164
|
"@eslint/core": "^0.16.0",
|
|
4161
4165
|
"@eslint/eslintrc": "^3.3.1",
|
|
4162
|
-
"@eslint/js": "9.
|
|
4166
|
+
"@eslint/js": "9.38.0",
|
|
4163
4167
|
"@eslint/plugin-kit": "^0.4.0",
|
|
4164
4168
|
"@humanfs/node": "^0.16.6",
|
|
4165
4169
|
"@humanwhocodes/module-importer": "^1.0.1",
|
|
4166
4170
|
"@humanwhocodes/retry": "^0.4.2",
|
|
4167
4171
|
"@types/estree": "^1.0.6",
|
|
4168
|
-
"@types/json-schema": "^7.0.15",
|
|
4169
4172
|
"ajv": "^6.12.4",
|
|
4170
4173
|
"chalk": "^4.0.0",
|
|
4171
4174
|
"cross-spawn": "^7.0.6",
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ module.exports = {
|
|
|
20
20
|
'enforce-css-module-identifier-casing': require('./rules/enforce-css-module-identifier-casing'),
|
|
21
21
|
'enforce-css-module-default-import': require('./rules/enforce-css-module-default-import'),
|
|
22
22
|
'use-styled-react-import': require('./rules/use-styled-react-import'),
|
|
23
|
+
'spread-props-first': require('./rules/spread-props-first'),
|
|
23
24
|
},
|
|
24
25
|
configs: {
|
|
25
26
|
recommended: require('./configs/recommended'),
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const rule = require('../spread-props-first')
|
|
2
|
+
const {RuleTester} = require('eslint')
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
ecmaVersion: 'latest',
|
|
7
|
+
sourceType: 'module',
|
|
8
|
+
parserOptions: {
|
|
9
|
+
ecmaFeatures: {
|
|
10
|
+
jsx: true,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
ruleTester.run('spread-props-first', rule, {
|
|
17
|
+
valid: [
|
|
18
|
+
// Spread props before named props
|
|
19
|
+
`<Example {...rest} className="foo" />`,
|
|
20
|
+
// Multiple spreads before named props
|
|
21
|
+
`<Example {...rest} {...other} className="foo" id="bar" />`,
|
|
22
|
+
// Only spread props
|
|
23
|
+
`<Example {...rest} />`,
|
|
24
|
+
// Only named props
|
|
25
|
+
`<Example className="foo" id="bar" />`,
|
|
26
|
+
// Empty element
|
|
27
|
+
`<Example />`,
|
|
28
|
+
// Spread first, then named props
|
|
29
|
+
`<Example {...rest} className="foo" onClick={handleClick} />`,
|
|
30
|
+
// Multiple spreads at the beginning
|
|
31
|
+
`<Example {...props1} {...props2} {...props3} className="foo" />`,
|
|
32
|
+
],
|
|
33
|
+
invalid: [
|
|
34
|
+
// Named prop before spread
|
|
35
|
+
{
|
|
36
|
+
code: `<Example className="foo" {...rest} />`,
|
|
37
|
+
output: `<Example {...rest} className="foo" />`,
|
|
38
|
+
errors: [
|
|
39
|
+
{
|
|
40
|
+
messageId: 'spreadPropsFirst',
|
|
41
|
+
data: {spreadProp: '{...rest}', namedProp: 'className'},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
// Multiple named props before spread
|
|
46
|
+
{
|
|
47
|
+
code: `<Example className="foo" id="bar" {...rest} />`,
|
|
48
|
+
output: `<Example {...rest} className="foo" id="bar" />`,
|
|
49
|
+
errors: [
|
|
50
|
+
{
|
|
51
|
+
messageId: 'spreadPropsFirst',
|
|
52
|
+
data: {spreadProp: '{...rest}', namedProp: 'id'},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
// Named prop with expression before spread
|
|
57
|
+
{
|
|
58
|
+
code: `<Example onClick={handleClick} {...rest} />`,
|
|
59
|
+
output: `<Example {...rest} onClick={handleClick} />`,
|
|
60
|
+
errors: [
|
|
61
|
+
{
|
|
62
|
+
messageId: 'spreadPropsFirst',
|
|
63
|
+
data: {spreadProp: '{...rest}', namedProp: 'onClick'},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
// Mixed order with multiple spreads
|
|
68
|
+
{
|
|
69
|
+
code: `<Example className="foo" {...rest} id="bar" {...other} />`,
|
|
70
|
+
output: `<Example {...rest} {...other} className="foo" id="bar" />`,
|
|
71
|
+
errors: [
|
|
72
|
+
{
|
|
73
|
+
messageId: 'spreadPropsFirst',
|
|
74
|
+
data: {spreadProp: '{...rest}', namedProp: 'id'},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
// Named prop before multiple spreads
|
|
79
|
+
{
|
|
80
|
+
code: `<Example className="foo" {...rest} {...other} />`,
|
|
81
|
+
output: `<Example {...rest} {...other} className="foo" />`,
|
|
82
|
+
errors: [
|
|
83
|
+
{
|
|
84
|
+
messageId: 'spreadPropsFirst',
|
|
85
|
+
data: {spreadProp: '{...rest}', namedProp: 'className'},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
// Complex example with many props
|
|
90
|
+
{
|
|
91
|
+
code: `<Example className="foo" id="bar" onClick={handleClick} {...rest} disabled />`,
|
|
92
|
+
output: `<Example {...rest} className="foo" id="bar" onClick={handleClick} disabled />`,
|
|
93
|
+
errors: [
|
|
94
|
+
{
|
|
95
|
+
messageId: 'spreadPropsFirst',
|
|
96
|
+
data: {spreadProp: '{...rest}', namedProp: 'disabled'},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
// Boolean prop before spread
|
|
101
|
+
{
|
|
102
|
+
code: `<Example disabled {...rest} />`,
|
|
103
|
+
output: `<Example {...rest} disabled />`,
|
|
104
|
+
errors: [
|
|
105
|
+
{
|
|
106
|
+
messageId: 'spreadPropsFirst',
|
|
107
|
+
data: {spreadProp: '{...rest}', namedProp: 'disabled'},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
// Spread in the middle
|
|
112
|
+
{
|
|
113
|
+
code: `<Example className="foo" {...rest} id="bar" />`,
|
|
114
|
+
output: `<Example {...rest} className="foo" id="bar" />`,
|
|
115
|
+
errors: [
|
|
116
|
+
{
|
|
117
|
+
messageId: 'spreadPropsFirst',
|
|
118
|
+
data: {spreadProp: '{...rest}', namedProp: 'id'},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
fixable: 'code',
|
|
5
|
+
schema: [],
|
|
6
|
+
messages: {
|
|
7
|
+
spreadPropsFirst:
|
|
8
|
+
'Spread props should come before other props to avoid unintentional overrides. Move {{spreadProp}} before {{namedProp}}.',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
create(context) {
|
|
12
|
+
return {
|
|
13
|
+
JSXOpeningElement(node) {
|
|
14
|
+
const attributes = node.attributes
|
|
15
|
+
|
|
16
|
+
// Track if we've seen a named prop before a spread
|
|
17
|
+
let lastNamedPropIndex = -1
|
|
18
|
+
let firstSpreadAfterNamedPropIndex = -1
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < attributes.length; i++) {
|
|
21
|
+
const attr = attributes[i]
|
|
22
|
+
|
|
23
|
+
if (attr.type === 'JSXAttribute') {
|
|
24
|
+
// This is a named prop
|
|
25
|
+
lastNamedPropIndex = i
|
|
26
|
+
} else if (attr.type === 'JSXSpreadAttribute' && lastNamedPropIndex !== -1) {
|
|
27
|
+
// This is a spread prop that comes after a named prop
|
|
28
|
+
if (firstSpreadAfterNamedPropIndex === -1) {
|
|
29
|
+
firstSpreadAfterNamedPropIndex = i
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If we found a spread after a named prop, report it
|
|
35
|
+
if (firstSpreadAfterNamedPropIndex !== -1) {
|
|
36
|
+
const sourceCode = context.sourceCode
|
|
37
|
+
const spreadAttr = attributes[firstSpreadAfterNamedPropIndex]
|
|
38
|
+
const namedAttr = attributes[lastNamedPropIndex]
|
|
39
|
+
|
|
40
|
+
context.report({
|
|
41
|
+
node: spreadAttr,
|
|
42
|
+
messageId: 'spreadPropsFirst',
|
|
43
|
+
data: {
|
|
44
|
+
spreadProp: sourceCode.getText(spreadAttr),
|
|
45
|
+
namedProp: namedAttr.name.name,
|
|
46
|
+
},
|
|
47
|
+
fix(fixer) {
|
|
48
|
+
// Collect all spreads and named props
|
|
49
|
+
const spreads = []
|
|
50
|
+
const namedProps = []
|
|
51
|
+
|
|
52
|
+
for (const attr of attributes) {
|
|
53
|
+
if (attr.type === 'JSXSpreadAttribute') {
|
|
54
|
+
spreads.push(attr)
|
|
55
|
+
} else if (attr.type === 'JSXAttribute') {
|
|
56
|
+
namedProps.push(attr)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Generate the reordered attributes text
|
|
61
|
+
const reorderedAttrs = [...spreads, ...namedProps]
|
|
62
|
+
const fixes = []
|
|
63
|
+
|
|
64
|
+
// Replace each attribute with its new position
|
|
65
|
+
for (let i = 0; i < attributes.length; i++) {
|
|
66
|
+
const newAttr = reorderedAttrs[i]
|
|
67
|
+
const oldAttr = attributes[i]
|
|
68
|
+
|
|
69
|
+
if (newAttr !== oldAttr) {
|
|
70
|
+
fixes.push(fixer.replaceText(oldAttr, sourceCode.getText(newAttr)))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return fixes
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}
|