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 CHANGED
@@ -6,7 +6,32 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) principles.
6
6
 
7
7
  ---
8
8
 
9
- ## [1.4.0] - 2026-01-30
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
- > **64 rules total** — 58 with auto-fix 🔧, 6 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
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 type/interface definitions |
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
- const checkArrowFunctionHandler = (node) => {
1094
- if (!isJsxAttributeArrowHandler(node)) return;
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
- if (statement.type !== "ExpressionStatement") return;
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
- const expression = statement.expression;
1141
+ const expression = statement.expression;
1107
1142
 
1108
- // Check if already on single line
1109
- if (expression.loc.start.line === expression.loc.end.line) {
1110
- const expressionText = sourceCode.getText(expression);
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
- context.report({
1113
- fix: (fixer) => fixer.replaceText(
1114
- node.body,
1115
- expressionText,
1116
- ),
1117
- message: "Arrow function with single return should use expression body: () => value instead of () => { return value }",
1118
- node: node.body,
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
- return;
1122
- }
1156
+ return;
1157
+ }
1123
1158
 
1124
- // Check if multi-line expression can be simplified to one line
1125
- if (canSimplifyToOneLineHandler(expression)) {
1126
- const simplifiedText = buildSimplifiedTextHandler(expression);
1159
+ // Check if multi-line expression can be simplified to one line
1160
+ if (canSimplifyToOneLineHandler(expression)) {
1161
+ const simplifiedText = buildSimplifiedTextHandler(expression);
1127
1162
 
1128
- context.report({
1129
- fix: (fixer) => fixer.replaceText(
1130
- node.body,
1131
- simplifiedText,
1132
- ),
1133
- message: "Arrow function with simple nested call should be simplified to one line",
1134
- node: node.body,
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
- // Check for call expression with multiline object/array argument
1141
- // e.g., setSorting({ by: "", number: 0, order: "" })
1142
- // This should become expression body but keep the multiline format
1143
- if (expression.type === "CallExpression") {
1144
- const expressionText = sourceCode.getText(expression);
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
- expressionText,
1234
+ returnText,
1150
1235
  ),
1151
- message: "Arrow function with single statement should use expression body: () => expression instead of () => { return expression }",
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 in JSX props with single statement block body" },
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 type or interface definitions.
5062
- * Types should be moved to a separate file (e.g., types.ts).
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
- TSInterfaceDeclaration(node) {
5085
- context.report({
5086
- message: "Interface definitions should not be in index files. Move to a types file.",
5087
- node,
5088
- });
5089
- },
5090
- TSTypeAliasDeclaration(node) {
5091
- context.report({
5092
- message: "Type definitions should not be in index files. Move to a types file.",
5093
- node,
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 type definitions" },
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 (current.type === "JSXAttribute"
6664
- || current.type === "VariableDeclarator"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.4.3",
3
+ "version": "1.5.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",