eslint-plugin-code-style 1.7.4 → 1.7.6

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +15 -10
  3. package/index.js +108 -153
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.7.6] - 2026-02-02
11
+
12
+ ### Changed
13
+
14
+ - **`ternary-condition-multiline`** - Now depends only on operand count, not line length:
15
+ - ≤maxOperands (default: 3): Always collapse to single line regardless of line length
16
+ - \>maxOperands: Format multiline with each operand on its own line
17
+ - Removed `maxLineLength` option (no longer used)
18
+ - This aligns behavior with `multiline-if-conditions` rule
19
+
20
+ ---
21
+
22
+ ## [1.7.5] - 2026-02-02
23
+
24
+ ### Fixed
25
+
26
+ - **`ternary-condition-multiline`** - For ≤3 operands, always collapse to single line when `?` is on different line than condition end (enforces `condition ? value : value` format for simple ternaries)
27
+ - **`no-empty-lines-in-function-params`** - Add detection for empty lines in TSTypeLiteral (type annotation objects like `{ prop: Type }` in intersection types)
28
+
29
+ ---
30
+
10
31
  ## [1.7.4] - 2026-02-02
11
32
 
12
33
  ### Fixed
@@ -1117,6 +1138,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1117
1138
 
1118
1139
  ---
1119
1140
 
1141
+ [1.7.6]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.5...v1.7.6
1142
+ [1.7.5]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.4...v1.7.5
1120
1143
  [1.7.4]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.3...v1.7.4
1121
1144
  [1.7.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.2...v1.7.3
1122
1145
  [1.7.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.1...v1.7.2
package/README.md CHANGED
@@ -295,7 +295,7 @@ rules: {
295
295
  | `if-statement-format` | `{` on same line as `if`/`else if`, `else` on same line as `}`, proper spacing 🔧 |
296
296
  | `multiline-if-conditions` | Conditions exceeding threshold get one operand per line with proper indentation (default: >3) 🔧 ⚙️ |
297
297
  | `no-empty-lines-in-switch-cases` | No empty line after `case X:` before code, no empty lines between cases 🔧 |
298
- | `ternary-condition-multiline` | Collapse simple ternaries to single line; expand complex conditions (>3 operands) to multiline 🔧 ⚙️ |
298
+ | `ternary-condition-multiline` | ≤maxOperands always single line; >maxOperands multiline (based on operand count, not line length) 🔧 ⚙️ |
299
299
  | **Function Rules** | |
300
300
  | `function-call-spacing` | No space between function name and `(`: `fn()` not `fn ()` 🔧 |
301
301
  | `function-declaration-style` | Auto-fix for `func-style`: converts function declarations to arrow expressions 🔧 |
@@ -1211,24 +1211,24 @@ switch (status) {
1211
1211
 
1212
1212
  ### `ternary-condition-multiline`
1213
1213
 
1214
- **What it does:** Enforces consistent ternary formatting:
1215
- - Simple ternaries (≤3 operands in condition) collapse to single line if they fit
1216
- - Complex ternaries (>3 operands) expand to multiline with each operand on its own line
1214
+ **What it does:** Formats ternary expressions based on condition operand count:
1215
+ - ≤maxOperands (default: 3): Always collapse to single line regardless of line length
1216
+ - \>maxOperands: Expand to multiline with each operand on its own line
1217
1217
 
1218
- **Why use it:** Long ternary conditions on a single line are hard to read. Breaking complex conditions into multiple lines makes them scannable.
1218
+ **Why use it:** Consistent formatting based on complexity, not line length. Simple conditions stay readable on one line; complex conditions get proper multiline formatting.
1219
1219
 
1220
1220
  **Options:**
1221
1221
 
1222
1222
  | Option | Type | Default | Description |
1223
1223
  |--------|------|---------|-------------|
1224
- | `maxOperands` | `integer` | `3` | Maximum operands to keep on single line |
1225
- | `maxLineLength` | `integer` | `120` | Maximum line length for single-line ternaries |
1224
+ | `maxOperands` | `integer` | `3` | Maximum condition operands to keep ternary on single line |
1226
1225
 
1227
1226
  ```javascript
1228
- // ✅ Good — simple condition on single line
1227
+ // ✅ Good — ≤3 operands always on single line
1229
1228
  const x = a && b && c ? "yes" : "no";
1229
+ const url = lang === "ar" ? `${apiEndpoints.exam.status}/${jobId}?lang=ar` : `${apiEndpoints.exam.status}/${jobId}`;
1230
1230
 
1231
- // ✅ Good — complex condition multiline (>3 operands)
1231
+ // ✅ Good — >3 operands formatted multiline
1232
1232
  const style = variant === "ghost"
1233
1233
  || variant === "ghost-danger"
1234
1234
  || variant === "muted"
@@ -1236,7 +1236,12 @@ const style = variant === "ghost"
1236
1236
  ? "transparent"
1237
1237
  : "solid";
1238
1238
 
1239
- // ❌ Bad — complex condition crammed on one line
1239
+ // ❌ Bad — ≤3 operands split across lines
1240
+ const x = a && b && c
1241
+ ? "yes"
1242
+ : "no";
1243
+
1244
+ // ❌ Bad — >3 operands crammed on one line
1240
1245
  const style = variant === "ghost" || variant === "ghost-danger" || variant === "muted" || variant === "primary" ? "transparent" : "solid";
1241
1246
  ```
1242
1247
 
package/index.js CHANGED
@@ -3868,21 +3868,31 @@ const multilineIfConditions = {
3868
3868
  * ───────────────────────────────────────────────────────────────
3869
3869
  *
3870
3870
  * Description:
3871
- * When a ternary condition has multiple operands (>3), format
3872
- * each operand on its own line for readability.
3871
+ * Formats ternary expressions based on condition operand count:
3872
+ * - ≤maxOperands (default: 3): Always collapse to single line
3873
+ * - >maxOperands: Format multiline with each operand on its own line
3873
3874
  *
3874
- * ✓ Good:
3875
+ * Options:
3876
+ * { maxOperands: 3 } - Maximum operands to keep on single line (default: 3)
3877
+ *
3878
+ * ✓ Good (≤3 operands - single line):
3875
3879
  * const x = a && b && c ? "yes" : "no";
3880
+ * const url = lang === "ar" ? "/ar/path" : "/en/path";
3876
3881
  *
3877
- * const x =
3878
- * variant === "ghost"
3882
+ * Good (>3 operands - multiline):
3883
+ * const x = variant === "ghost"
3879
3884
  * || variant === "ghost-danger"
3880
3885
  * || variant === "muted"
3881
3886
  * || variant === "primary"
3882
- * ? "value1"
3883
- * : "value2";
3887
+ * ? "value1"
3888
+ * : "value2";
3884
3889
  *
3885
- * ✗ Bad:
3890
+ * ✗ Bad (≤3 operands split across lines):
3891
+ * const x = a && b && c
3892
+ * ? "yes"
3893
+ * : "no";
3894
+ *
3895
+ * ✗ Bad (>3 operands on single line):
3886
3896
  * const x = variant === "ghost" || variant === "ghost-danger" || variant === "muted" || variant === "primary" ? "value1" : "value2";
3887
3897
  */
3888
3898
  const ternaryConditionMultiline = {
@@ -3890,7 +3900,6 @@ const ternaryConditionMultiline = {
3890
3900
  const sourceCode = context.sourceCode || context.getSourceCode();
3891
3901
  const options = context.options[0] || {};
3892
3902
  const maxOperands = options.maxOperands ?? 3;
3893
- const maxLineLength = options.maxLineLength ?? 120;
3894
3903
 
3895
3904
  // Check if node is wrapped in parentheses
3896
3905
  const isParenthesizedHandler = (node) => {
@@ -4048,7 +4057,7 @@ const ternaryConditionMultiline = {
4048
4057
  return false;
4049
4058
  };
4050
4059
 
4051
- // Handle simple ternaries - collapse to single line if they fit
4060
+ // Handle simple ternaries (≤maxOperands) - always collapse to single line
4052
4061
  const handleSimpleTernaryHandler = (node) => {
4053
4062
  const isOnSingleLine = node.loc.start.line === node.loc.end.line;
4054
4063
  const hasOperatorOnOwnLine = isOperatorOnOwnLineHandler(node);
@@ -4068,46 +4077,15 @@ const ternaryConditionMultiline = {
4068
4077
 
4069
4078
  // Calculate what the single line would look like
4070
4079
  const singleLineText = getTernarySingleLineHandler(node);
4071
- const indent = getLineIndentHandler(node);
4072
-
4073
- // Check if the parent needs prefix text (like "const x = " or "key: ")
4074
- let prefixLength = 0;
4075
- const parent = node.parent;
4076
-
4077
- if (parent && parent.type === "VariableDeclarator" && parent.init === node) {
4078
- // Calculate prefix based on parent's start line (where "const x = " is)
4079
- const varDecl = parent.parent;
4080
- const declKeyword = varDecl ? sourceCode.getFirstToken(varDecl).value : "const";
4081
- const varName = parent.id.name || sourceCode.getText(parent.id);
4082
-
4083
- // Prefix is "const varName = " or similar
4084
- prefixLength = declKeyword.length + 1 + varName.length + 3; // keyword + space + name + " = "
4085
- } else if (parent && parent.type === "AssignmentExpression" && parent.right === node) {
4086
- // Calculate prefix based on left side of assignment
4087
- const leftText = sourceCode.getText(parent.left);
4088
-
4089
- prefixLength = leftText.length + 3; // left + " = "
4090
- } else if (parent && parent.type === "Property" && parent.value === node) {
4091
- // Object property: key: ternary
4092
- const keyText = sourceCode.getText(parent.key);
4093
-
4094
- prefixLength = keyText.length + 2; // key + ": "
4095
- }
4096
-
4097
- // Check if single line would fit
4098
- const totalLength = indent + prefixLength + singleLineText.length + 1;
4099
-
4100
- if (totalLength <= maxLineLength) {
4101
- context.report({
4102
- fix: (fixer) => fixer.replaceText(node, singleLineText),
4103
- message: "Simple ternary should be on a single line",
4104
- node,
4105
- });
4106
4080
 
4107
- return true;
4108
- }
4081
+ // For ≤maxOperands conditions, always collapse to single line regardless of length
4082
+ context.report({
4083
+ fix: (fixer) => fixer.replaceText(node, singleLineText),
4084
+ message: `Ternary with ≤${maxOperands} operands should be on a single line`,
4085
+ node,
4086
+ });
4109
4087
 
4110
- return false;
4088
+ return true;
4111
4089
  };
4112
4090
 
4113
4091
  // Handle complex logical expressions - format multiline
@@ -4118,112 +4096,35 @@ const ternaryConditionMultiline = {
4118
4096
  const testEndLine = test.loc.end.line;
4119
4097
  const isMultiLine = testStartLine !== testEndLine;
4120
4098
 
4121
- // ≤maxOperands operands: keep condition on single line, and try to collapse whole ternary
4099
+ // ≤maxOperands operands: always collapse to single line (regardless of line length)
4122
4100
  if (operands.length <= maxOperands) {
4123
- const firstOperandStartLine = operands[0].loc.start.line;
4124
- const allOperandsStartOnSameLine = operands.every(
4125
- (op) => op.loc.start.line === firstOperandStartLine,
4126
- );
4127
-
4128
- const hasSplitBinaryExpression = operands.some(
4129
- (op) => isBinaryExpressionSplitHandler(op),
4130
- );
4131
-
4132
- // Check if ? or : is on its own line without its value
4133
- const hasOperatorOnOwnLine = isOperatorOnOwnLineHandler(node);
4134
-
4135
- // Check if ternary is multiline (could be collapsed)
4136
- const isTernaryMultiline = node.loc.start.line !== node.loc.end.line;
4137
-
4138
- // Helper to build single line condition
4139
- const buildSameLineHandler = (n) => {
4140
- if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
4141
- const leftText = buildSameLineHandler(n.left);
4142
- const rightText = buildSameLineHandler(n.right);
4143
-
4144
- return `${leftText} ${n.operator} ${rightText}`;
4145
- }
4146
-
4147
- if (n.type === "BinaryExpression" && isBinaryExpressionSplitHandler(n)) {
4148
- return buildBinaryExpressionSingleLineHandler(n);
4149
- }
4150
-
4151
- return getSourceTextWithGroupsHandler(n);
4152
- };
4153
-
4154
- // Check if whole ternary can fit on one line
4155
- const singleLineText = getTernarySingleLineHandler(node);
4156
- const indent = getLineIndentHandler(node);
4157
-
4158
- // Calculate prefix length for context
4159
- let prefixLength = 0;
4160
- const parent = node.parent;
4161
-
4162
- if (parent && parent.type === "VariableDeclarator" && parent.init === node) {
4163
- const varDecl = parent.parent;
4164
- const declKeyword = varDecl ? sourceCode.getFirstToken(varDecl).value : "const";
4165
- const varName = parent.id.name || sourceCode.getText(parent.id);
4166
-
4167
- prefixLength = declKeyword.length + 1 + varName.length + 3;
4168
- } else if (parent && parent.type === "AssignmentExpression" && parent.right === node) {
4169
- const leftText = sourceCode.getText(parent.left);
4170
-
4171
- prefixLength = leftText.length + 3;
4172
- } else if (parent && parent.type === "Property" && parent.value === node) {
4173
- const keyText = sourceCode.getText(parent.key);
4174
-
4175
- prefixLength = keyText.length + 2;
4176
- }
4177
-
4178
- const totalLength = indent + prefixLength + singleLineText.length + 1;
4179
- const canFitOnOneLine = totalLength <= maxLineLength;
4180
-
4181
4101
  // Skip if branches have complex objects
4182
4102
  const hasComplexBranches = hasComplexObjectHandler(node.consequent) || hasComplexObjectHandler(node.alternate);
4183
4103
 
4184
4104
  // Skip nested ternaries
4185
4105
  const hasNestedTernary = node.consequent.type === "ConditionalExpression" || node.alternate.type === "ConditionalExpression";
4186
4106
 
4187
- // Determine if we need to fix anything
4188
- const needsConditionFix = !allOperandsStartOnSameLine || hasSplitBinaryExpression;
4189
- const needsTernaryCollapse = isTernaryMultiline && canFitOnOneLine && !hasComplexBranches && !hasNestedTernary;
4190
- const needsOperatorFix = hasOperatorOnOwnLine;
4107
+ if (hasComplexBranches || hasNestedTernary) {
4108
+ return;
4109
+ }
4191
4110
 
4192
- if (needsConditionFix || needsTernaryCollapse || needsOperatorFix) {
4193
- // If whole ternary can fit on one line, collapse it
4194
- if (canFitOnOneLine && !hasComplexBranches && !hasNestedTernary) {
4195
- context.report({
4196
- fix: (fixer) => fixer.replaceText(node, singleLineText),
4197
- message: `Ternary with ≤${maxOperands} operands should be on single line when it fits`,
4198
- node,
4199
- });
4200
- } else if (needsOperatorFix) {
4201
- // Format as proper multiline with ? and : on their own lines with values
4202
- context.report({
4203
- fix: (fixer) => {
4204
- const lineText = sourceCode.lines[node.loc.start.line - 1];
4205
- const baseIndent = lineText.match(/^\s*/)[0];
4206
- const conditionIndent = baseIndent + " ";
4207
- const conditionText = buildSameLineHandler(test);
4208
- const consequentText = sourceCode.getText(node.consequent);
4209
- const alternateText = sourceCode.getText(node.alternate);
4210
- const newText = `${conditionText}\n${conditionIndent}? ${consequentText}\n${conditionIndent}: ${alternateText}`;
4211
-
4212
- return fixer.replaceText(node, newText);
4213
- },
4214
- message: `Ternary with ≤${maxOperands} operands should have ? and : with their values on the same line`,
4215
- node,
4216
- });
4217
- } else if (needsConditionFix) {
4218
- // Otherwise just fix the condition to be on single line
4219
- context.report({
4220
- fix: (fixer) => fixer.replaceText(test, buildSameLineHandler(test)),
4221
- message: `Ternary conditions with ≤${maxOperands} operands should be single line`,
4222
- node: test,
4223
- });
4224
- }
4111
+ // Check if already properly formatted (single line)
4112
+ const isOnSingleLine = node.loc.start.line === node.loc.end.line;
4113
+ const hasOperatorOnOwnLine = isOperatorOnOwnLineHandler(node);
4114
+
4115
+ if (isOnSingleLine && !hasOperatorOnOwnLine) {
4116
+ return;
4225
4117
  }
4226
4118
 
4119
+ // Collapse to single line
4120
+ const singleLineText = getTernarySingleLineHandler(node);
4121
+
4122
+ context.report({
4123
+ fix: (fixer) => fixer.replaceText(node, singleLineText),
4124
+ message: `Ternary with ≤${maxOperands} operands should be on a single line`,
4125
+ node,
4126
+ });
4127
+
4227
4128
  return;
4228
4129
  }
4229
4130
 
@@ -4379,21 +4280,15 @@ const ternaryConditionMultiline = {
4379
4280
  };
4380
4281
  },
4381
4282
  meta: {
4382
- docs: { description: "Enforce consistent ternary formatting: collapse simple ternaries to single line, expand complex conditions to multiline" },
4283
+ docs: { description: "Enforce consistent ternary formatting based on condition operand count: ≤maxOperands collapses to single line, >maxOperands expands to multiline" },
4383
4284
  fixable: "code",
4384
4285
  schema: [
4385
4286
  {
4386
4287
  additionalProperties: false,
4387
4288
  properties: {
4388
- maxLineLength: {
4389
- default: 120,
4390
- description: "Maximum line length for single-line ternaries (default: 120)",
4391
- minimum: 80,
4392
- type: "integer",
4393
- },
4394
4289
  maxOperands: {
4395
4290
  default: 3,
4396
- description: "Maximum operands to keep on single line (default: 3)",
4291
+ description: "Maximum condition operands to keep ternary on single line (default: 3). Ternaries with more operands are formatted multiline.",
4397
4292
  minimum: 1,
4398
4293
  type: "integer",
4399
4294
  },
@@ -9857,14 +9752,74 @@ const noEmptyLinesInFunctionParams = {
9857
9752
  });
9858
9753
  };
9859
9754
 
9755
+ // Check TSTypeLiteral for empty lines (type annotation objects like { prop: Type })
9756
+ const checkTypeLiteralHandler = (node) => {
9757
+ if (!node.members || node.members.length === 0) return;
9758
+
9759
+ const firstMember = node.members[0];
9760
+ const lastMember = node.members[node.members.length - 1];
9761
+
9762
+ // Find opening brace
9763
+ const openBrace = sourceCode.getFirstToken(node);
9764
+
9765
+ if (openBrace && openBrace.value === "{") {
9766
+ // Check for empty line after opening brace
9767
+ if (firstMember.loc.start.line - openBrace.loc.end.line > 1) {
9768
+ context.report({
9769
+ fix: (fixer) => fixer.replaceTextRange(
9770
+ [openBrace.range[1], firstMember.range[0]],
9771
+ "\n" + " ".repeat(firstMember.loc.start.column),
9772
+ ),
9773
+ message: "No empty line after opening brace in type definition",
9774
+ node: firstMember,
9775
+ });
9776
+ }
9777
+ }
9778
+
9779
+ // Find closing brace
9780
+ const closeBrace = sourceCode.getLastToken(node);
9781
+
9782
+ if (closeBrace && closeBrace.value === "}") {
9783
+ // Check for empty line before closing brace
9784
+ if (closeBrace.loc.start.line - lastMember.loc.end.line > 1) {
9785
+ context.report({
9786
+ fix: (fixer) => fixer.replaceTextRange(
9787
+ [lastMember.range[1], closeBrace.range[0]],
9788
+ "\n" + " ".repeat(closeBrace.loc.start.column),
9789
+ ),
9790
+ message: "No empty line before closing brace in type definition",
9791
+ node: lastMember,
9792
+ });
9793
+ }
9794
+ }
9795
+
9796
+ // Check for empty lines between members
9797
+ for (let i = 0; i < node.members.length - 1; i += 1) {
9798
+ const current = node.members[i];
9799
+ const next = node.members[i + 1];
9800
+
9801
+ if (next.loc.start.line - current.loc.end.line > 1) {
9802
+ context.report({
9803
+ fix: (fixer) => fixer.replaceTextRange(
9804
+ [current.range[1], next.range[0]],
9805
+ "\n" + " ".repeat(next.loc.start.column),
9806
+ ),
9807
+ message: "No empty lines between type members",
9808
+ node: next,
9809
+ });
9810
+ }
9811
+ }
9812
+ };
9813
+
9860
9814
  return {
9861
9815
  ArrowFunctionExpression: checkFunctionHandler,
9862
9816
  FunctionDeclaration: checkFunctionHandler,
9863
9817
  FunctionExpression: checkFunctionHandler,
9818
+ TSTypeLiteral: checkTypeLiteralHandler,
9864
9819
  };
9865
9820
  },
9866
9821
  meta: {
9867
- docs: { description: "Disallow empty lines in function parameters" },
9822
+ docs: { description: "Disallow empty lines in function parameters and type definitions" },
9868
9823
  fixable: "whitespace",
9869
9824
  schema: [],
9870
9825
  type: "layout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
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",