eslint-plugin-code-style 1.4.5 → 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 +25 -0
- package/README.md +44 -1
- package/index.d.ts +2 -0
- package/index.js +391 -38
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) principles.
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
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
|
+
|
|
9
34
|
## [1.4.2] - 2026-01-30
|
|
10
35
|
|
|
11
36
|
**Release Title:** New Rules, Enhanced Auto-Fix & Comprehensive Documentation
|
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 🔧 |
|
|
@@ -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
|
+
}
|
|
1095
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
|
+
};
|
|
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);
|
|
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
|
+
}
|
|
1145
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
|
|
@@ -10076,6 +10424,10 @@ const openingBracketsSameLine = {
|
|
|
10076
10424
|
|
|
10077
10425
|
// Case 2: Arrow function callback
|
|
10078
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
|
+
|
|
10079
10431
|
const arrowParams = firstArg.params;
|
|
10080
10432
|
|
|
10081
10433
|
if (arrowParams.length === 0) return;
|
|
@@ -15841,6 +16193,7 @@ export default {
|
|
|
15841
16193
|
|
|
15842
16194
|
// Control flow rules
|
|
15843
16195
|
"block-statement-newlines": blockStatementNewlines,
|
|
16196
|
+
"if-else-spacing": ifElseSpacing,
|
|
15844
16197
|
"if-statement-format": ifStatementFormat,
|
|
15845
16198
|
"multiline-if-conditions": multilineIfConditions,
|
|
15846
16199
|
"no-empty-lines-in-switch-cases": noEmptyLinesInSwitchCases,
|
package/package.json
CHANGED