eslint-plugin-code-style 1.4.3 → 1.5.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 +26 -1
- package/README.md +45 -2
- package/index.d.ts +2 -0
- package/index.js +533 -58
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,32 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) principles.
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## [1.
|
|
9
|
+
## [1.5.0] - 2026-01-30
|
|
10
|
+
|
|
11
|
+
**Release Title:** New if-else-spacing Rule & Enhanced Arrow/Class Method Support
|
|
12
|
+
|
|
13
|
+
**Version Range:** v1.4.3 → v1.5.0
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **New Rule: `if-else-spacing`** - Enforces proper spacing between if statements:
|
|
18
|
+
- Requires empty line between consecutive if statements with block bodies
|
|
19
|
+
- Prevents empty lines between single-line if and else
|
|
20
|
+
|
|
21
|
+
### Enhanced
|
|
22
|
+
|
|
23
|
+
- **`function-naming-convention`** - Now checks class methods for Handler suffix (skips constructors, getters, setters, and React lifecycle methods)
|
|
24
|
+
- **`arrow-function-simplify`** - Extended to handle ALL arrow functions with single return (not just JSX attributes): `() => { return x }` becomes `() => x`
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Circular fix conflict** between `opening-brackets-same-line` and `function-arguments-format` for multi-argument arrow function calls (e.g., axios interceptors)
|
|
29
|
+
|
|
30
|
+
**Full Changelog:** https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.4.3...v1.5.0
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## [1.4.2] - 2026-01-30
|
|
10
35
|
|
|
11
36
|
**Release Title:** New Rules, Enhanced Auto-Fix & Comprehensive Documentation
|
|
12
37
|
|
package/README.md
CHANGED
|
@@ -206,6 +206,7 @@ rules: {
|
|
|
206
206
|
"code-style/function-params-per-line": "error",
|
|
207
207
|
"code-style/hook-callback-format": "error",
|
|
208
208
|
"code-style/hook-deps-per-line": "error",
|
|
209
|
+
"code-style/if-else-spacing": "error",
|
|
209
210
|
"code-style/if-statement-format": "error",
|
|
210
211
|
"code-style/import-format": "error",
|
|
211
212
|
"code-style/import-source-spacing": "error",
|
|
@@ -250,7 +251,7 @@ rules: {
|
|
|
250
251
|
|
|
251
252
|
## 📖 Rules Categories
|
|
252
253
|
|
|
253
|
-
> **
|
|
254
|
+
> **65 rules total** — 59 with auto-fix 🔧, 6 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
|
|
254
255
|
>
|
|
255
256
|
> **Legend:** 🔧 Auto-fixable with `eslint --fix` • ⚙️ Customizable options
|
|
256
257
|
|
|
@@ -278,6 +279,7 @@ rules: {
|
|
|
278
279
|
| `component-props-inline-type` | Inline type annotation `} : {` with matching props, proper spacing, commas, no interface reference 🔧 |
|
|
279
280
|
| **Control Flow Rules** | |
|
|
280
281
|
| `block-statement-newlines` | Newline after `{` and before `}` in if/for/while/function blocks 🔧 |
|
|
282
|
+
| `if-else-spacing` | Empty line between consecutive if blocks, no empty line between single-line if/else 🔧 |
|
|
281
283
|
| `if-statement-format` | `{` on same line as `if`/`else if`, `else` on same line as `}`, proper spacing 🔧 |
|
|
282
284
|
| `multiline-if-conditions` | Conditions exceeding threshold get one operand per line with proper indentation (default: >3) 🔧 ⚙️ |
|
|
283
285
|
| `no-empty-lines-in-switch-cases` | No empty line after `case X:` before code, no empty lines between cases 🔧 |
|
|
@@ -298,7 +300,7 @@ rules: {
|
|
|
298
300
|
| `import-format` | `import {` and `} from` on same line; collapse ≤ threshold; expand larger with each specifier on own line (default: ≤3) 🔧 ⚙️ |
|
|
299
301
|
| `import-source-spacing` | No leading/trailing spaces inside import path quotes 🔧 |
|
|
300
302
|
| `index-export-style` | Index files: no blank lines, enforce shorthand or import-export style; Regular files: require blank lines between exports (default: shorthand) 🔧 ⚙️ |
|
|
301
|
-
| `index-exports-only` | Index files should only contain imports and exports, not
|
|
303
|
+
| `index-exports-only` | Index files should only contain imports and re-exports, not code definitions (types, functions, variables, classes) |
|
|
302
304
|
| `module-index-exports` | Index files must export all folder contents (files and subfolders) ⚙️ |
|
|
303
305
|
| **JSX Rules** | |
|
|
304
306
|
| `classname-dynamic-at-end` | Dynamic expressions (`${className}`) must be at the end of class strings (JSX and variables) 🔧 |
|
|
@@ -878,6 +880,47 @@ for (const item of items) { process(item);
|
|
|
878
880
|
|
|
879
881
|
---
|
|
880
882
|
|
|
883
|
+
### `if-else-spacing`
|
|
884
|
+
|
|
885
|
+
**What it does:** Enforces proper spacing between if statements and if-else chains:
|
|
886
|
+
- Consecutive if statements with block bodies must have an empty line between them
|
|
887
|
+
- Single-line if and else should NOT have empty lines between them
|
|
888
|
+
|
|
889
|
+
**Why use it:** Maintains visual separation between distinct conditional blocks while keeping related single-line if-else pairs compact.
|
|
890
|
+
|
|
891
|
+
```javascript
|
|
892
|
+
// ✅ Good — empty line between consecutive if blocks
|
|
893
|
+
if (!hasValidParams) return null;
|
|
894
|
+
|
|
895
|
+
if (status === "loading") {
|
|
896
|
+
return <Loading />;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (status === "error") {
|
|
900
|
+
return <Error />;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// ✅ Good — no empty line between single-line if-else
|
|
904
|
+
if (error) prom.reject(error);
|
|
905
|
+
else prom.resolve(token);
|
|
906
|
+
|
|
907
|
+
// ❌ Bad — no empty line between if blocks
|
|
908
|
+
if (!hasValidParams) return null;
|
|
909
|
+
if (status === "loading") {
|
|
910
|
+
return <Loading />;
|
|
911
|
+
}
|
|
912
|
+
if (status === "error") {
|
|
913
|
+
return <Error />;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ❌ Bad — empty line between single-line if-else
|
|
917
|
+
if (error) prom.reject(error);
|
|
918
|
+
|
|
919
|
+
else prom.resolve(token);
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
881
924
|
### `if-statement-format`
|
|
882
925
|
|
|
883
926
|
**What it does:** Enforces consistent if/else formatting:
|
package/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type RuleNames =
|
|
|
26
26
|
| "code-style/function-params-per-line"
|
|
27
27
|
| "code-style/hook-callback-format"
|
|
28
28
|
| "code-style/hook-deps-per-line"
|
|
29
|
+
| "code-style/if-else-spacing"
|
|
29
30
|
| "code-style/if-statement-format"
|
|
30
31
|
| "code-style/import-format"
|
|
31
32
|
| "code-style/import-source-spacing"
|
|
@@ -112,6 +113,7 @@ interface PluginRules {
|
|
|
112
113
|
"function-params-per-line": Rule.RuleModule;
|
|
113
114
|
"hook-callback-format": Rule.RuleModule;
|
|
114
115
|
"hook-deps-per-line": Rule.RuleModule;
|
|
116
|
+
"if-else-spacing": Rule.RuleModule;
|
|
115
117
|
"if-statement-format": Rule.RuleModule;
|
|
116
118
|
"import-format": Rule.RuleModule;
|
|
117
119
|
"import-source-spacing": Rule.RuleModule;
|
package/index.js
CHANGED
|
@@ -1090,9 +1090,41 @@ const arrowFunctionSimplify = {
|
|
|
1090
1090
|
return `${calleeName}(${argsText})`;
|
|
1091
1091
|
};
|
|
1092
1092
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1093
|
+
// Check if an expression is simple enough to be on one line
|
|
1094
|
+
const isSimpleExpressionHandler = (expr) => {
|
|
1095
|
+
if (!expr) return false;
|
|
1096
|
+
|
|
1097
|
+
// Simple types that can always be one-lined
|
|
1098
|
+
if (expr.type === "Identifier") return true;
|
|
1099
|
+
if (expr.type === "Literal") return true;
|
|
1100
|
+
if (expr.type === "ThisExpression") return true;
|
|
1101
|
+
|
|
1102
|
+
// Member expressions: obj.prop, this.value
|
|
1103
|
+
if (expr.type === "MemberExpression") {
|
|
1104
|
+
return isSimpleExpressionHandler(expr.object) && isSimpleExpressionHandler(expr.property);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Unary expressions: !x, -x
|
|
1108
|
+
if (expr.type === "UnaryExpression") {
|
|
1109
|
+
return isSimpleExpressionHandler(expr.argument);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Simple call expressions with simple arguments
|
|
1113
|
+
if (expr.type === "CallExpression") {
|
|
1114
|
+
if (expr.arguments.length > 2) return false;
|
|
1115
|
+
|
|
1116
|
+
return expr.arguments.every(isSimpleExpressionHandler);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Object/Array expressions - keep multiline format but still simplify
|
|
1120
|
+
if (expr.type === "ObjectExpression" || expr.type === "ArrayExpression") {
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return false;
|
|
1125
|
+
};
|
|
1095
1126
|
|
|
1127
|
+
const checkArrowFunctionHandler = (node) => {
|
|
1096
1128
|
if (node.body.type !== "BlockStatement") return;
|
|
1097
1129
|
|
|
1098
1130
|
const { body } = node.body;
|
|
@@ -1101,54 +1133,107 @@ const arrowFunctionSimplify = {
|
|
|
1101
1133
|
|
|
1102
1134
|
const statement = body[0];
|
|
1103
1135
|
|
|
1104
|
-
|
|
1136
|
+
// Handle ExpressionStatement (for JSX attributes like onClick={() => { doSomething() }})
|
|
1137
|
+
if (statement.type === "ExpressionStatement") {
|
|
1138
|
+
// Only for JSX attributes - non-JSX expression statements without return are side effects
|
|
1139
|
+
if (!isJsxAttributeArrowHandler(node)) return;
|
|
1105
1140
|
|
|
1106
|
-
|
|
1141
|
+
const expression = statement.expression;
|
|
1107
1142
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1143
|
+
// Check if already on single line
|
|
1144
|
+
if (expression.loc.start.line === expression.loc.end.line) {
|
|
1145
|
+
const expressionText = sourceCode.getText(expression);
|
|
1111
1146
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1147
|
+
context.report({
|
|
1148
|
+
fix: (fixer) => fixer.replaceText(
|
|
1149
|
+
node.body,
|
|
1150
|
+
expressionText,
|
|
1151
|
+
),
|
|
1152
|
+
message: "Arrow function with single statement should use expression body: () => expression instead of () => { expression }",
|
|
1153
|
+
node: node.body,
|
|
1154
|
+
});
|
|
1120
1155
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1123
1158
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1159
|
+
// Check if multi-line expression can be simplified to one line
|
|
1160
|
+
if (canSimplifyToOneLineHandler(expression)) {
|
|
1161
|
+
const simplifiedText = buildSimplifiedTextHandler(expression);
|
|
1127
1162
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1163
|
+
context.report({
|
|
1164
|
+
fix: (fixer) => fixer.replaceText(
|
|
1165
|
+
node.body,
|
|
1166
|
+
simplifiedText,
|
|
1167
|
+
),
|
|
1168
|
+
message: "Arrow function with simple nested call should be simplified to one line",
|
|
1169
|
+
node: node.body,
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Check for call expression with multiline object/array argument
|
|
1176
|
+
if (expression.type === "CallExpression") {
|
|
1177
|
+
const expressionText = sourceCode.getText(expression);
|
|
1178
|
+
|
|
1179
|
+
context.report({
|
|
1180
|
+
fix: (fixer) => fixer.replaceText(
|
|
1181
|
+
node.body,
|
|
1182
|
+
expressionText,
|
|
1183
|
+
),
|
|
1184
|
+
message: "Arrow function with single statement should use expression body",
|
|
1185
|
+
node: node.body,
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1136
1188
|
|
|
1137
1189
|
return;
|
|
1138
1190
|
}
|
|
1139
1191
|
|
|
1140
|
-
//
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1192
|
+
// Handle ReturnStatement: () => { return x } should become () => x
|
|
1193
|
+
if (statement.type === "ReturnStatement") {
|
|
1194
|
+
const returnValue = statement.argument;
|
|
1195
|
+
|
|
1196
|
+
// No return value: () => { return; } - keep as is (explicit void return)
|
|
1197
|
+
if (!returnValue) return;
|
|
1198
|
+
|
|
1199
|
+
// Check if the return value is simple enough to inline
|
|
1200
|
+
const returnText = sourceCode.getText(returnValue);
|
|
1145
1201
|
|
|
1202
|
+
// For object literals, wrap in parentheses: () => ({ key: value })
|
|
1203
|
+
if (returnValue.type === "ObjectExpression") {
|
|
1204
|
+
context.report({
|
|
1205
|
+
fix: (fixer) => fixer.replaceText(
|
|
1206
|
+
node.body,
|
|
1207
|
+
`(${returnText})`,
|
|
1208
|
+
),
|
|
1209
|
+
message: "Arrow function with single return should use expression body: () => value instead of () => { return value }",
|
|
1210
|
+
node: node.body,
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// For simple expressions, just use the value directly
|
|
1217
|
+
if (isSimpleExpressionHandler(returnValue)) {
|
|
1218
|
+
context.report({
|
|
1219
|
+
fix: (fixer) => fixer.replaceText(
|
|
1220
|
+
node.body,
|
|
1221
|
+
returnText,
|
|
1222
|
+
),
|
|
1223
|
+
message: "Arrow function with single return should use expression body: () => value instead of () => { return value }",
|
|
1224
|
+
node: node.body,
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// For other expressions (JSX, complex calls, etc.), still simplify but keep formatting
|
|
1146
1231
|
context.report({
|
|
1147
1232
|
fix: (fixer) => fixer.replaceText(
|
|
1148
1233
|
node.body,
|
|
1149
|
-
|
|
1234
|
+
returnText,
|
|
1150
1235
|
),
|
|
1151
|
-
message: "Arrow function with single
|
|
1236
|
+
message: "Arrow function with single return should use expression body: () => value instead of () => { return value }",
|
|
1152
1237
|
node: node.body,
|
|
1153
1238
|
});
|
|
1154
1239
|
}
|
|
@@ -1198,7 +1283,7 @@ const arrowFunctionSimplify = {
|
|
|
1198
1283
|
};
|
|
1199
1284
|
},
|
|
1200
1285
|
meta: {
|
|
1201
|
-
docs: { description: "Simplify arrow functions
|
|
1286
|
+
docs: { description: "Simplify arrow functions with single return to expression body: () => { return x } becomes () => x" },
|
|
1202
1287
|
fixable: "code",
|
|
1203
1288
|
schema: [],
|
|
1204
1289
|
type: "layout",
|
|
@@ -1925,16 +2010,19 @@ const functionNamingConvention = {
|
|
|
1925
2010
|
|
|
1926
2011
|
const checkFunctionHandler = (node) => {
|
|
1927
2012
|
let name = null;
|
|
2013
|
+
let identifierNode = null;
|
|
1928
2014
|
|
|
1929
2015
|
if (node.type === "FunctionDeclaration" && node.id) {
|
|
1930
2016
|
name = node.id.name;
|
|
2017
|
+
identifierNode = node.id;
|
|
1931
2018
|
} else if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
1932
2019
|
if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id) {
|
|
1933
2020
|
name = node.parent.id.name;
|
|
2021
|
+
identifierNode = node.parent.id;
|
|
1934
2022
|
}
|
|
1935
2023
|
}
|
|
1936
2024
|
|
|
1937
|
-
if (!name) return;
|
|
2025
|
+
if (!name || !identifierNode) return;
|
|
1938
2026
|
|
|
1939
2027
|
// Skip hooks
|
|
1940
2028
|
if (/^use[A-Z]/.test(name)) return;
|
|
@@ -2043,14 +2131,116 @@ const functionNamingConvention = {
|
|
|
2043
2131
|
}
|
|
2044
2132
|
};
|
|
2045
2133
|
|
|
2134
|
+
// Check class methods (MethodDefinition)
|
|
2135
|
+
const checkMethodHandler = (node) => {
|
|
2136
|
+
// Skip constructors
|
|
2137
|
+
if (node.kind === "constructor") return;
|
|
2138
|
+
|
|
2139
|
+
// Skip getters and setters - they're property accessors, not action methods
|
|
2140
|
+
if (node.kind === "get" || node.kind === "set") return;
|
|
2141
|
+
|
|
2142
|
+
const { key } = node;
|
|
2143
|
+
|
|
2144
|
+
// Only check methods with Identifier keys (skip computed properties like [Symbol.iterator])
|
|
2145
|
+
if (key.type !== "Identifier") return;
|
|
2146
|
+
|
|
2147
|
+
const name = key.name;
|
|
2148
|
+
|
|
2149
|
+
// Skip hooks
|
|
2150
|
+
if (/^use[A-Z]/.test(name)) return;
|
|
2151
|
+
|
|
2152
|
+
// Skip React lifecycle methods
|
|
2153
|
+
const lifecycleMethods = [
|
|
2154
|
+
"render", "componentDidMount", "componentDidUpdate", "componentWillUnmount",
|
|
2155
|
+
"shouldComponentUpdate", "getSnapshotBeforeUpdate", "componentDidCatch",
|
|
2156
|
+
"getDerivedStateFromProps", "getDerivedStateFromError",
|
|
2157
|
+
];
|
|
2158
|
+
|
|
2159
|
+
if (lifecycleMethods.includes(name)) return;
|
|
2160
|
+
|
|
2161
|
+
const hasVerbPrefix = startsWithVerbHandler(name);
|
|
2162
|
+
const hasHandlerSuffix = endsWithHandler(name);
|
|
2163
|
+
|
|
2164
|
+
if (!hasVerbPrefix && !hasHandlerSuffix) {
|
|
2165
|
+
context.report({
|
|
2166
|
+
message: `Method "${name}" should start with a verb (get, set, fetch, handle, etc.) AND end with "Handler" (e.g., getDataHandler, handleClickHandler)`,
|
|
2167
|
+
node: key,
|
|
2168
|
+
});
|
|
2169
|
+
} else if (!hasVerbPrefix) {
|
|
2170
|
+
context.report({
|
|
2171
|
+
message: `Method "${name}" should start with a verb (get, set, fetch, handle, click, submit, etc.)`,
|
|
2172
|
+
node: key,
|
|
2173
|
+
});
|
|
2174
|
+
} else if (!hasHandlerSuffix) {
|
|
2175
|
+
const newName = `${name}Handler`;
|
|
2176
|
+
|
|
2177
|
+
context.report({
|
|
2178
|
+
fix(fixer) {
|
|
2179
|
+
// For class methods, we need to find all references manually
|
|
2180
|
+
// This is simpler than functions since class methods are typically accessed via this.methodName
|
|
2181
|
+
const fixes = [fixer.replaceText(key, newName)];
|
|
2182
|
+
|
|
2183
|
+
// Find all references to this method in the class body
|
|
2184
|
+
const classBody = node.parent;
|
|
2185
|
+
|
|
2186
|
+
if (classBody && classBody.type === "ClassBody") {
|
|
2187
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
2188
|
+
const classText = sourceCode.getText(classBody);
|
|
2189
|
+
|
|
2190
|
+
// Find usages like this.methodName or super.methodName
|
|
2191
|
+
const classNode = classBody.parent;
|
|
2192
|
+
|
|
2193
|
+
if (classNode) {
|
|
2194
|
+
const searchPatternHandler = (n) => {
|
|
2195
|
+
if (n.type === "MemberExpression" &&
|
|
2196
|
+
n.property.type === "Identifier" &&
|
|
2197
|
+
n.property.name === name &&
|
|
2198
|
+
(n.object.type === "ThisExpression" || n.object.type === "Super")) {
|
|
2199
|
+
// Don't fix the definition itself
|
|
2200
|
+
if (n.property !== key) {
|
|
2201
|
+
fixes.push(fixer.replaceText(n.property, newName));
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// Recursively search
|
|
2206
|
+
for (const childKey of Object.keys(n)) {
|
|
2207
|
+
const child = n[childKey];
|
|
2208
|
+
|
|
2209
|
+
if (child && typeof child === "object") {
|
|
2210
|
+
if (Array.isArray(child)) {
|
|
2211
|
+
child.forEach((item) => {
|
|
2212
|
+
if (item && typeof item === "object" && item.type) {
|
|
2213
|
+
searchPatternHandler(item);
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
} else if (child.type) {
|
|
2217
|
+
searchPatternHandler(child);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
|
|
2223
|
+
searchPatternHandler(classNode);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
return fixes;
|
|
2228
|
+
},
|
|
2229
|
+
message: `Method "${name}" should end with "Handler" suffix (e.g., ${newName})`,
|
|
2230
|
+
node: key,
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2046
2235
|
return {
|
|
2047
2236
|
ArrowFunctionExpression: checkFunctionHandler,
|
|
2048
2237
|
FunctionDeclaration: checkFunctionHandler,
|
|
2049
2238
|
FunctionExpression: checkFunctionHandler,
|
|
2239
|
+
MethodDefinition: checkMethodHandler,
|
|
2050
2240
|
};
|
|
2051
2241
|
},
|
|
2052
2242
|
meta: {
|
|
2053
|
-
docs: { description: "Enforce function names to start with a verb AND end with Handler" },
|
|
2243
|
+
docs: { description: "Enforce function and method names to start with a verb AND end with Handler" },
|
|
2054
2244
|
fixable: "code",
|
|
2055
2245
|
schema: [],
|
|
2056
2246
|
type: "suggestion",
|
|
@@ -2968,6 +3158,164 @@ const ifStatementFormat = {
|
|
|
2968
3158
|
},
|
|
2969
3159
|
};
|
|
2970
3160
|
|
|
3161
|
+
/**
|
|
3162
|
+
* ───────────────────────────────────────────────────────────────
|
|
3163
|
+
* Rule: If-Else Spacing
|
|
3164
|
+
* ───────────────────────────────────────────────────────────────
|
|
3165
|
+
*
|
|
3166
|
+
* Description:
|
|
3167
|
+
* Enforces proper spacing between if statements and if-else chains:
|
|
3168
|
+
* 1. Consecutive if statements with block bodies must have an empty line between them
|
|
3169
|
+
* 2. Single-line if and else should NOT have empty lines between them
|
|
3170
|
+
*
|
|
3171
|
+
* ✓ Good:
|
|
3172
|
+
* if (!hasValidParams) return null;
|
|
3173
|
+
*
|
|
3174
|
+
* if (status === "loading") {
|
|
3175
|
+
* return <Loading />;
|
|
3176
|
+
* }
|
|
3177
|
+
*
|
|
3178
|
+
* if (status === "error") {
|
|
3179
|
+
* return <Error />;
|
|
3180
|
+
* }
|
|
3181
|
+
*
|
|
3182
|
+
* if (error) prom.reject(error);
|
|
3183
|
+
* else prom.resolve(token);
|
|
3184
|
+
*
|
|
3185
|
+
* ✗ Bad:
|
|
3186
|
+
* if (!hasValidParams) return null;
|
|
3187
|
+
* if (status === "loading") {
|
|
3188
|
+
* return <Loading />;
|
|
3189
|
+
* }
|
|
3190
|
+
* if (status === "error") {
|
|
3191
|
+
* return <Error />;
|
|
3192
|
+
* }
|
|
3193
|
+
*
|
|
3194
|
+
* if (error) prom.reject(error);
|
|
3195
|
+
*
|
|
3196
|
+
* else prom.resolve(token);
|
|
3197
|
+
*/
|
|
3198
|
+
const ifElseSpacing = {
|
|
3199
|
+
create(context) {
|
|
3200
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
3201
|
+
|
|
3202
|
+
// Check if an if statement is single-line (no block body or block on same line)
|
|
3203
|
+
const isSingleLineIfHandler = (node) => {
|
|
3204
|
+
if (node.type !== "IfStatement") return false;
|
|
3205
|
+
|
|
3206
|
+
const { consequent } = node;
|
|
3207
|
+
|
|
3208
|
+
// If consequent is not a block, it's single-line
|
|
3209
|
+
if (consequent.type !== "BlockStatement") return true;
|
|
3210
|
+
|
|
3211
|
+
// If it's a block, check if the entire block is on one line
|
|
3212
|
+
return consequent.loc.start.line === consequent.loc.end.line;
|
|
3213
|
+
};
|
|
3214
|
+
|
|
3215
|
+
// Check single-line if-else should not have empty lines between if and else
|
|
3216
|
+
const checkSingleLineIfElseHandler = (node) => {
|
|
3217
|
+
const { alternate } = node;
|
|
3218
|
+
|
|
3219
|
+
if (!alternate) return;
|
|
3220
|
+
|
|
3221
|
+
// Only check single-line if statements
|
|
3222
|
+
if (!isSingleLineIfHandler(node)) return;
|
|
3223
|
+
|
|
3224
|
+
// Find the closing of the consequent
|
|
3225
|
+
const { consequent } = node;
|
|
3226
|
+
|
|
3227
|
+
const closingToken = consequent.type === "BlockStatement"
|
|
3228
|
+
? sourceCode.getLastToken(consequent)
|
|
3229
|
+
: sourceCode.getLastToken(consequent);
|
|
3230
|
+
|
|
3231
|
+
// Find the else keyword
|
|
3232
|
+
const elseKeyword = sourceCode.getTokenAfter(
|
|
3233
|
+
closingToken,
|
|
3234
|
+
(t) => t.value === "else",
|
|
3235
|
+
);
|
|
3236
|
+
|
|
3237
|
+
if (!elseKeyword) return;
|
|
3238
|
+
|
|
3239
|
+
// Check if there's an empty line between the consequent and else
|
|
3240
|
+
const linesBetween = elseKeyword.loc.start.line - closingToken.loc.end.line;
|
|
3241
|
+
|
|
3242
|
+
if (linesBetween > 1) {
|
|
3243
|
+
context.report({
|
|
3244
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
3245
|
+
[closingToken.range[1], elseKeyword.range[0]],
|
|
3246
|
+
"\n" + " ".repeat(elseKeyword.loc.start.column),
|
|
3247
|
+
),
|
|
3248
|
+
message: "No empty line allowed between single-line if and else",
|
|
3249
|
+
node: elseKeyword,
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
};
|
|
3253
|
+
|
|
3254
|
+
// Check consecutive if statements in a block
|
|
3255
|
+
const checkConsecutiveIfsHandler = (node) => {
|
|
3256
|
+
const { body } = node;
|
|
3257
|
+
|
|
3258
|
+
if (!body || !Array.isArray(body)) return;
|
|
3259
|
+
|
|
3260
|
+
for (let i = 0; i < body.length - 1; i += 1) {
|
|
3261
|
+
const current = body[i];
|
|
3262
|
+
const next = body[i + 1];
|
|
3263
|
+
|
|
3264
|
+
// Only check if current is an if statement
|
|
3265
|
+
if (current.type !== "IfStatement") continue;
|
|
3266
|
+
|
|
3267
|
+
// Only check if next is an if statement
|
|
3268
|
+
if (next.type !== "IfStatement") continue;
|
|
3269
|
+
|
|
3270
|
+
// Get the actual end of current (could be alternate/else-if chain)
|
|
3271
|
+
let currentEnd = current;
|
|
3272
|
+
|
|
3273
|
+
while (currentEnd.alternate && currentEnd.alternate.type === "IfStatement") {
|
|
3274
|
+
currentEnd = currentEnd.alternate;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// If current ends with an else block (not else-if), use the alternate
|
|
3278
|
+
const endNode = currentEnd.alternate || currentEnd.consequent;
|
|
3279
|
+
|
|
3280
|
+
// Check if either the current if (or its last branch) has a block body
|
|
3281
|
+
const currentHasBlock = endNode.type === "BlockStatement";
|
|
3282
|
+
|
|
3283
|
+
// Check if the next if has a block body
|
|
3284
|
+
const nextHasBlock = next.consequent.type === "BlockStatement";
|
|
3285
|
+
|
|
3286
|
+
// Require empty line if either has a block body
|
|
3287
|
+
if (currentHasBlock || nextHasBlock) {
|
|
3288
|
+
const linesBetween = next.loc.start.line - endNode.loc.end.line;
|
|
3289
|
+
|
|
3290
|
+
if (linesBetween === 1) {
|
|
3291
|
+
// No empty line between them - needs one
|
|
3292
|
+
context.report({
|
|
3293
|
+
fix: (fixer) => fixer.insertTextAfter(
|
|
3294
|
+
endNode,
|
|
3295
|
+
"\n",
|
|
3296
|
+
),
|
|
3297
|
+
message: "Expected empty line between consecutive if statements with block bodies",
|
|
3298
|
+
node: next,
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
|
|
3305
|
+
return {
|
|
3306
|
+
BlockStatement: checkConsecutiveIfsHandler,
|
|
3307
|
+
IfStatement: checkSingleLineIfElseHandler,
|
|
3308
|
+
Program: checkConsecutiveIfsHandler,
|
|
3309
|
+
};
|
|
3310
|
+
},
|
|
3311
|
+
meta: {
|
|
3312
|
+
docs: { description: "Enforce proper spacing between if statements and if-else chains" },
|
|
3313
|
+
fixable: "whitespace",
|
|
3314
|
+
schema: [],
|
|
3315
|
+
type: "layout",
|
|
3316
|
+
},
|
|
3317
|
+
};
|
|
3318
|
+
|
|
2971
3319
|
/**
|
|
2972
3320
|
* ───────────────────────────────────────────────────────────────
|
|
2973
3321
|
* Rule: Multiline If Conditions
|
|
@@ -5058,18 +5406,23 @@ const indexExportStyle = {
|
|
|
5058
5406
|
*
|
|
5059
5407
|
* Description:
|
|
5060
5408
|
* Index files (index.ts, index.tsx, index.js, index.jsx) should
|
|
5061
|
-
* only contain imports and exports, not
|
|
5062
|
-
*
|
|
5409
|
+
* only contain imports and re-exports, not any code definitions.
|
|
5410
|
+
* All definitions (types, interfaces, functions, variables, classes)
|
|
5411
|
+
* should be moved to separate files.
|
|
5063
5412
|
*
|
|
5064
5413
|
* ✓ Good:
|
|
5065
5414
|
* // index.ts
|
|
5066
5415
|
* export { Button } from "./Button";
|
|
5416
|
+
* export { helper } from "./utils";
|
|
5067
5417
|
* export type { ButtonProps } from "./types";
|
|
5418
|
+
* export * from "./constants";
|
|
5068
5419
|
*
|
|
5069
5420
|
* ✗ Bad:
|
|
5070
5421
|
* // index.ts
|
|
5071
5422
|
* export type ButtonVariant = "primary" | "secondary";
|
|
5072
5423
|
* export interface ButtonProps { ... }
|
|
5424
|
+
* export const CONSTANT = "value";
|
|
5425
|
+
* export function helper() { ... }
|
|
5073
5426
|
*/
|
|
5074
5427
|
const indexExportsOnly = {
|
|
5075
5428
|
create(context) {
|
|
@@ -5080,23 +5433,76 @@ const indexExportsOnly = {
|
|
|
5080
5433
|
|
|
5081
5434
|
if (!isIndexFile) return {};
|
|
5082
5435
|
|
|
5436
|
+
// Helper to check if a node is an import or export statement
|
|
5437
|
+
const isImportOrExportHandler = (node) => {
|
|
5438
|
+
const { type } = node;
|
|
5439
|
+
|
|
5440
|
+
// Import statements
|
|
5441
|
+
if (type === "ImportDeclaration") return true;
|
|
5442
|
+
|
|
5443
|
+
// Export statements (named, default, all)
|
|
5444
|
+
if (type === "ExportNamedDeclaration") {
|
|
5445
|
+
// Only allow re-exports (export { x } from "./module" or export { x })
|
|
5446
|
+
// If it has a declaration, it's defining something (not allowed)
|
|
5447
|
+
return !node.declaration;
|
|
5448
|
+
}
|
|
5449
|
+
|
|
5450
|
+
if (type === "ExportDefaultDeclaration") {
|
|
5451
|
+
// Allow export default <identifier> (re-exporting)
|
|
5452
|
+
// Disallow export default function/class/object (defining something)
|
|
5453
|
+
return node.declaration && node.declaration.type === "Identifier";
|
|
5454
|
+
}
|
|
5455
|
+
|
|
5456
|
+
if (type === "ExportAllDeclaration") return true;
|
|
5457
|
+
|
|
5458
|
+
return false;
|
|
5459
|
+
};
|
|
5460
|
+
|
|
5461
|
+
// Get a friendly description of what the disallowed node is
|
|
5462
|
+
const getNodeDescriptionHandler = (node) => {
|
|
5463
|
+
switch (node.type) {
|
|
5464
|
+
case "TSTypeAliasDeclaration":
|
|
5465
|
+
return "Type definition";
|
|
5466
|
+
case "TSInterfaceDeclaration":
|
|
5467
|
+
return "Interface definition";
|
|
5468
|
+
case "TSEnumDeclaration":
|
|
5469
|
+
return "Enum definition";
|
|
5470
|
+
case "VariableDeclaration":
|
|
5471
|
+
return "Variable declaration";
|
|
5472
|
+
case "FunctionDeclaration":
|
|
5473
|
+
return "Function declaration";
|
|
5474
|
+
case "ClassDeclaration":
|
|
5475
|
+
return "Class declaration";
|
|
5476
|
+
case "ExportNamedDeclaration":
|
|
5477
|
+
if (node.declaration) {
|
|
5478
|
+
return getNodeDescriptionHandler(node.declaration);
|
|
5479
|
+
}
|
|
5480
|
+
|
|
5481
|
+
return "Export with inline declaration";
|
|
5482
|
+
case "ExportDefaultDeclaration":
|
|
5483
|
+
return "Default export with inline definition";
|
|
5484
|
+
default:
|
|
5485
|
+
return "Code";
|
|
5486
|
+
}
|
|
5487
|
+
};
|
|
5488
|
+
|
|
5083
5489
|
return {
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
}
|
|
5490
|
+
Program(programNode) {
|
|
5491
|
+
for (const node of programNode.body) {
|
|
5492
|
+
if (!isImportOrExportHandler(node)) {
|
|
5493
|
+
const description = getNodeDescriptionHandler(node);
|
|
5494
|
+
|
|
5495
|
+
context.report({
|
|
5496
|
+
message: `${description} should not be in index files. Index files should only contain imports and re-exports. Move this to a separate file.`,
|
|
5497
|
+
node,
|
|
5498
|
+
});
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5095
5501
|
},
|
|
5096
5502
|
};
|
|
5097
5503
|
},
|
|
5098
5504
|
meta: {
|
|
5099
|
-
docs: { description: "Index files should only contain imports and exports, not
|
|
5505
|
+
docs: { description: "Index files should only contain imports and re-exports, not code definitions" },
|
|
5100
5506
|
schema: [],
|
|
5101
5507
|
type: "suggestion",
|
|
5102
5508
|
},
|
|
@@ -6660,24 +7066,25 @@ const classNameMultiline = {
|
|
|
6660
7066
|
let current = node;
|
|
6661
7067
|
|
|
6662
7068
|
while (current) {
|
|
6663
|
-
if
|
|
6664
|
-
|
|
6665
|
-
|| current.type === "Property") {
|
|
7069
|
+
// For JSX attributes, check if inline and use column-based indent
|
|
7070
|
+
if (current.type === "JSXAttribute") {
|
|
6666
7071
|
const lineIndent = getLineIndent(current);
|
|
6667
7072
|
const lineText = sourceCode.lines[current.loc.start.line - 1];
|
|
6668
|
-
|
|
6669
|
-
// Check if there's content before the node on this line
|
|
6670
7073
|
const contentBefore = lineText.slice(0, current.loc.start.column).trim();
|
|
6671
7074
|
|
|
6672
7075
|
if (contentBefore) {
|
|
6673
7076
|
// Attribute is inline (e.g., <Component className=...>)
|
|
6674
|
-
// Use column position as indent for proper alignment
|
|
6675
7077
|
return " ".repeat(current.loc.start.column);
|
|
6676
7078
|
}
|
|
6677
7079
|
|
|
6678
7080
|
return lineIndent;
|
|
6679
7081
|
}
|
|
6680
7082
|
|
|
7083
|
+
// For variables and properties, just use line indentation
|
|
7084
|
+
if (current.type === "VariableDeclarator" || current.type === "Property") {
|
|
7085
|
+
return getLineIndent(current);
|
|
7086
|
+
}
|
|
7087
|
+
|
|
6681
7088
|
current = current.parent;
|
|
6682
7089
|
}
|
|
6683
7090
|
|
|
@@ -10017,6 +10424,10 @@ const openingBracketsSameLine = {
|
|
|
10017
10424
|
|
|
10018
10425
|
// Case 2: Arrow function callback
|
|
10019
10426
|
if (firstArg.type === "ArrowFunctionExpression") {
|
|
10427
|
+
// Skip if there are multiple arguments - function-arguments-format handles that case
|
|
10428
|
+
// to avoid circular fixes where this rule wants fn((param) and that rule wants fn(\n (param)
|
|
10429
|
+
if (args.length > 1) return;
|
|
10430
|
+
|
|
10020
10431
|
const arrowParams = firstArg.params;
|
|
10021
10432
|
|
|
10022
10433
|
if (arrowParams.length === 0) return;
|
|
@@ -12215,6 +12626,38 @@ const functionObjectDestructure = {
|
|
|
12215
12626
|
return destructured;
|
|
12216
12627
|
};
|
|
12217
12628
|
|
|
12629
|
+
// Find the destructuring statement and property node for a variable
|
|
12630
|
+
const findDestructuringStatementHandler = (blockBody, varName, paramName) => {
|
|
12631
|
+
if (blockBody.type !== "BlockStatement") return null;
|
|
12632
|
+
|
|
12633
|
+
for (const stmt of blockBody.body) {
|
|
12634
|
+
if (stmt.type !== "VariableDeclaration") continue;
|
|
12635
|
+
|
|
12636
|
+
for (const decl of stmt.declarations) {
|
|
12637
|
+
if (decl.id.type === "ObjectPattern" && decl.init) {
|
|
12638
|
+
// Check if this destructuring creates the variable we're looking for
|
|
12639
|
+
if (decl.init.type === "Identifier" && decl.init.name === paramName) {
|
|
12640
|
+
for (const prop of decl.id.properties) {
|
|
12641
|
+
if (prop.type === "Property" && prop.key.type === "Identifier") {
|
|
12642
|
+
const createdVarName = prop.value.type === "Identifier" ? prop.value.name : prop.key.name;
|
|
12643
|
+
|
|
12644
|
+
if (createdVarName === varName) {
|
|
12645
|
+
return {
|
|
12646
|
+
declarator: decl,
|
|
12647
|
+
property: prop,
|
|
12648
|
+
statement: stmt,
|
|
12649
|
+
};
|
|
12650
|
+
}
|
|
12651
|
+
}
|
|
12652
|
+
}
|
|
12653
|
+
}
|
|
12654
|
+
}
|
|
12655
|
+
}
|
|
12656
|
+
}
|
|
12657
|
+
|
|
12658
|
+
return null;
|
|
12659
|
+
};
|
|
12660
|
+
|
|
12218
12661
|
const checkFunctionHandler = (node) => {
|
|
12219
12662
|
const isComponent = isReactComponentHandler(node);
|
|
12220
12663
|
const params = node.params;
|
|
@@ -12327,7 +12770,29 @@ const functionObjectDestructure = {
|
|
|
12327
12770
|
if (accesses.length > 0) {
|
|
12328
12771
|
const accessedProps = [...new Set(accesses.map((a) => a.property))];
|
|
12329
12772
|
|
|
12773
|
+
// Find the original destructuring statement
|
|
12774
|
+
const destructInfo = findDestructuringStatementHandler(body, varName, firstParam.name);
|
|
12775
|
+
|
|
12330
12776
|
context.report({
|
|
12777
|
+
fix: destructInfo
|
|
12778
|
+
? (fixer) => {
|
|
12779
|
+
const fixes = [];
|
|
12780
|
+
|
|
12781
|
+
// Modify the destructuring property to use nested destructuring
|
|
12782
|
+
// e.g., { target } becomes { target: { value } }
|
|
12783
|
+
const { property } = destructInfo;
|
|
12784
|
+
const nestedDestructure = `${info.originalProp}: { ${accessedProps.join(", ")} }`;
|
|
12785
|
+
|
|
12786
|
+
fixes.push(fixer.replaceText(property, nestedDestructure));
|
|
12787
|
+
|
|
12788
|
+
// Replace all varName.prop accesses with just prop
|
|
12789
|
+
accesses.forEach((access) => {
|
|
12790
|
+
fixes.push(fixer.replaceText(access.node, access.property));
|
|
12791
|
+
});
|
|
12792
|
+
|
|
12793
|
+
return fixes;
|
|
12794
|
+
}
|
|
12795
|
+
: undefined,
|
|
12331
12796
|
message: `Variable "${varName}" is accessed via dot notation (${accessedProps.join(", ")}). Use nested destructuring instead: "const { ${info.originalProp}: { ${accessedProps.join(", ")} } } = ..."`,
|
|
12332
12797
|
node: accesses[0].node,
|
|
12333
12798
|
});
|
|
@@ -13138,6 +13603,15 @@ const noInlineTypeDefinitions = {
|
|
|
13138
13603
|
return;
|
|
13139
13604
|
}
|
|
13140
13605
|
|
|
13606
|
+
// Handle intersection types (e.g., ButtonHTMLAttributes & { variant: "a" | "b" })
|
|
13607
|
+
if (typeNode.type === "TSIntersectionType") {
|
|
13608
|
+
for (const type of typeNode.types) {
|
|
13609
|
+
checkTypeAnnotationHandler(type, paramName);
|
|
13610
|
+
}
|
|
13611
|
+
|
|
13612
|
+
return;
|
|
13613
|
+
}
|
|
13614
|
+
|
|
13141
13615
|
// Handle object types with union properties
|
|
13142
13616
|
if (typeNode.type === "TSTypeLiteral") {
|
|
13143
13617
|
for (const member of typeNode.members) {
|
|
@@ -15719,6 +16193,7 @@ export default {
|
|
|
15719
16193
|
|
|
15720
16194
|
// Control flow rules
|
|
15721
16195
|
"block-statement-newlines": blockStatementNewlines,
|
|
16196
|
+
"if-else-spacing": ifElseSpacing,
|
|
15722
16197
|
"if-statement-format": ifStatementFormat,
|
|
15723
16198
|
"multiline-if-conditions": multilineIfConditions,
|
|
15724
16199
|
"no-empty-lines-in-switch-cases": noEmptyLinesInSwitchCases,
|
package/package.json
CHANGED