eslint-plugin-code-style 1.8.1 → 1.8.3

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 +24 -0
  2. package/README.md +80 -2
  3. package/index.js +924 -54
  4. package/package.json +1 -1
package/index.js CHANGED
@@ -3258,8 +3258,15 @@ const ifStatementFormat = {
3258
3258
  }
3259
3259
 
3260
3260
  // LogicalExpression - collapse if it's short enough to fit on one line
3261
- // "Short enough" means under 80 characters for the condition itself
3261
+ // BUT skip if it has intentional multiline formatting (operator at start of line)
3262
3262
  if (testNode.type === "LogicalExpression") {
3263
+ // Check for intentional multiline format (operator at start of line after newline)
3264
+ // This pattern indicates multiline-if-conditions has intentionally formatted it
3265
+ if (/\n\s*(\|\||&&)/.test(testText)) {
3266
+ return false; // Intentionally multilined, don't collapse
3267
+ }
3268
+
3269
+ // "Short enough" means under 80 characters for the condition itself
3263
3270
  const conditionLength = testText.replace(/\s+/g, " ").trim().length;
3264
3271
 
3265
3272
  return conditionLength <= 80;
@@ -3518,26 +3525,36 @@ const ifElseSpacing = {
3518
3525
  * Description:
3519
3526
  * When an if statement has multiple conditions that span
3520
3527
  * multiple lines, each condition should be on its own line.
3528
+ * Nested groups with >maxOperands are formatted multiline inline.
3529
+ * Groups exceeding nesting level 2 are extracted to variables.
3521
3530
  *
3522
3531
  * Options:
3523
3532
  * { maxOperands: 3 } - Maximum operands on single line (default: 3)
3524
3533
  *
3525
- * ✓ Good:
3526
- * if (
3527
- * conditionA &&
3528
- * conditionB &&
3529
- * conditionC
3530
- * ) {}
3531
- *
3532
- * ✗ Bad:
3533
- * if (conditionA &&
3534
- * conditionB && conditionC) {}
3534
+ * ✓ Good (nested group with >3 operands - multiline inline):
3535
+ * if ((
3536
+ * a
3537
+ * || b
3538
+ * || c
3539
+ * || d
3540
+ * ) && e) {}
3541
+ *
3542
+ * ✓ Good (≤2 nesting level):
3543
+ * if ((a && b) || c) {} // Level 1
3544
+ * if ((a && (b || c)) || d) {} // Level 2
3545
+ *
3546
+ * ✗ Bad (>2 nesting level - auto-fixed by extraction):
3547
+ * if ((a && (b || (c && d))) || e) {}
3548
+ * // Fixed to:
3549
+ * // const isCAndD = (c && d);
3550
+ * // if ((a && (b || isCAndD)) || e) {}
3535
3551
  */
3536
3552
  const multilineIfConditions = {
3537
3553
  create(context) {
3538
3554
  const sourceCode = context.sourceCode || context.getSourceCode();
3539
3555
  const options = context.options[0] || {};
3540
3556
  const maxOperands = options.maxOperands !== undefined ? options.maxOperands : 3;
3557
+ const maxNestingLevel = 2; // Fixed at 2 - not configurable to avoid overly complex conditions
3541
3558
 
3542
3559
  const isParenthesizedHandler = (node) => {
3543
3560
  const tokenBefore = sourceCode.getTokenBefore(node);
@@ -3598,6 +3615,138 @@ const multilineIfConditions = {
3598
3615
  return operands;
3599
3616
  };
3600
3617
 
3618
+ // Calculate max nesting depth of parenthesized groups in a logical expression
3619
+ // Level 0: a && b (no groups)
3620
+ // Level 1: (a && b) || c (one group at top)
3621
+ // Level 2: (a && (b || c)) || d (group inside group)
3622
+ const getNestingDepthHandler = (node, currentDepth = 0) => {
3623
+ if (node.type !== "LogicalExpression") return currentDepth;
3624
+
3625
+ let maxDepth = currentDepth;
3626
+
3627
+ // Check left side
3628
+ if (node.left.type === "LogicalExpression") {
3629
+ const leftDepth = isParenthesizedHandler(node.left)
3630
+ ? getNestingDepthHandler(node.left, currentDepth + 1)
3631
+ : getNestingDepthHandler(node.left, currentDepth);
3632
+ maxDepth = Math.max(maxDepth, leftDepth);
3633
+ }
3634
+
3635
+ // Check right side
3636
+ if (node.right.type === "LogicalExpression") {
3637
+ const rightDepth = isParenthesizedHandler(node.right)
3638
+ ? getNestingDepthHandler(node.right, currentDepth + 1)
3639
+ : getNestingDepthHandler(node.right, currentDepth);
3640
+ maxDepth = Math.max(maxDepth, rightDepth);
3641
+ }
3642
+
3643
+ return maxDepth;
3644
+ };
3645
+
3646
+ // Find the deepest nested parenthesized group that exceeds maxNestingLevel
3647
+ const findDeepNestedGroupHandler = (node, currentDepth = 0, parentInfo = null) => {
3648
+ if (node.type !== "LogicalExpression") return null;
3649
+
3650
+ let deepestGroup = null;
3651
+
3652
+ // Check left side
3653
+ if (node.left.type === "LogicalExpression") {
3654
+ const leftIsParen = isParenthesizedHandler(node.left);
3655
+ const newDepth = leftIsParen ? currentDepth + 1 : currentDepth;
3656
+
3657
+ if (leftIsParen && newDepth > maxNestingLevel) {
3658
+ // This group exceeds max nesting
3659
+ deepestGroup = {
3660
+ node: node.left,
3661
+ depth: newDepth,
3662
+ parent: node,
3663
+ side: "left",
3664
+ };
3665
+ }
3666
+
3667
+ // Recurse to find even deeper
3668
+ const deeper = findDeepNestedGroupHandler(node.left, newDepth, { node, side: "left" });
3669
+ if (deeper && (!deepestGroup || deeper.depth > deepestGroup.depth)) {
3670
+ deepestGroup = deeper;
3671
+ }
3672
+ }
3673
+
3674
+ // Check right side
3675
+ if (node.right.type === "LogicalExpression") {
3676
+ const rightIsParen = isParenthesizedHandler(node.right);
3677
+ const newDepth = rightIsParen ? currentDepth + 1 : currentDepth;
3678
+
3679
+ if (rightIsParen && newDepth > maxNestingLevel) {
3680
+ // This group exceeds max nesting
3681
+ const rightGroup = {
3682
+ node: node.right,
3683
+ depth: newDepth,
3684
+ parent: node,
3685
+ side: "right",
3686
+ };
3687
+ if (!deepestGroup || rightGroup.depth > deepestGroup.depth) {
3688
+ deepestGroup = rightGroup;
3689
+ }
3690
+ }
3691
+
3692
+ // Recurse to find even deeper
3693
+ const deeper = findDeepNestedGroupHandler(node.right, newDepth, { node, side: "right" });
3694
+ if (deeper && (!deepestGroup || deeper.depth > deepestGroup.depth)) {
3695
+ deepestGroup = deeper;
3696
+ }
3697
+ }
3698
+
3699
+ return deepestGroup;
3700
+ };
3701
+
3702
+ // Count operands inside a group, ignoring the outer parentheses of the group itself
3703
+ // This is used to count operands INSIDE a parenthesized group
3704
+ const countOperandsInsideGroupHandler = (node) => {
3705
+ if (node.type !== "LogicalExpression") return 1;
3706
+
3707
+ let count = 0;
3708
+ const countHelperHandler = (n) => {
3709
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
3710
+ countHelperHandler(n.left);
3711
+ countHelperHandler(n.right);
3712
+ } else {
3713
+ count += 1;
3714
+ }
3715
+ };
3716
+
3717
+ // Start by recursing into children directly (ignoring outer parens)
3718
+ countHelperHandler(node.left);
3719
+ countHelperHandler(node.right);
3720
+
3721
+ return count;
3722
+ };
3723
+
3724
+ // Find a nested parenthesized group that has >maxOperands operands
3725
+ // Returns the first such group found (for extraction to variable)
3726
+ const findNestedGroupExceedingMaxOperandsHandler = (node) => {
3727
+ if (node.type !== "LogicalExpression") return null;
3728
+
3729
+ // Check if this node is a parenthesized group with >maxOperands
3730
+ if (isParenthesizedHandler(node)) {
3731
+ const insideCount = countOperandsInsideGroupHandler(node);
3732
+ if (insideCount > maxOperands) {
3733
+ return node;
3734
+ }
3735
+ }
3736
+
3737
+ // Recursively check children
3738
+ if (node.left.type === "LogicalExpression") {
3739
+ const found = findNestedGroupExceedingMaxOperandsHandler(node.left);
3740
+ if (found) return found;
3741
+ }
3742
+ if (node.right.type === "LogicalExpression") {
3743
+ const found = findNestedGroupExceedingMaxOperandsHandler(node.right);
3744
+ if (found) return found;
3745
+ }
3746
+
3747
+ return null;
3748
+ };
3749
+
3601
3750
  const checkIfStatementHandler = (node) => {
3602
3751
  const { test } = node;
3603
3752
 
@@ -3609,6 +3758,81 @@ const multilineIfConditions = {
3609
3758
 
3610
3759
  if (!openParen || !closeParen) return;
3611
3760
 
3761
+ // Check for excessive nesting depth
3762
+ const nestingDepth = getNestingDepthHandler(test);
3763
+
3764
+ if (nestingDepth > maxNestingLevel) {
3765
+ const deepGroup = findDeepNestedGroupHandler(test);
3766
+
3767
+ if (deepGroup) {
3768
+ const groupText = getSourceTextWithGroupsHandler(deepGroup.node);
3769
+
3770
+ // Generate a more descriptive variable name based on condition structure
3771
+ const getConditionNameHandler = (n) => {
3772
+ if (n.type === "LogicalExpression") {
3773
+ const leftName = getConditionNameHandler(n.left);
3774
+ const rightName = getConditionNameHandler(n.right);
3775
+ const opName = n.operator === "&&" ? "And" : "Or";
3776
+
3777
+ return `${leftName}${opName}${rightName}`;
3778
+ }
3779
+ if (n.type === "Identifier") {
3780
+ return n.name.charAt(0).toUpperCase() + n.name.slice(1);
3781
+ }
3782
+ if (n.type === "BinaryExpression" || n.type === "CallExpression" || n.type === "MemberExpression") {
3783
+ return "Expr";
3784
+ }
3785
+
3786
+ return "Cond";
3787
+ };
3788
+
3789
+ // Generate unique variable name
3790
+ let varName = `is${getConditionNameHandler(deepGroup.node)}`;
3791
+ // Limit length and sanitize
3792
+ if (varName.length > 30) {
3793
+ varName = "isNestedCondition";
3794
+ }
3795
+
3796
+ context.report({
3797
+ fix: (fixer) => {
3798
+ const fixes = [];
3799
+
3800
+ // Get the line before the if statement for inserting the variable
3801
+ const ifLine = node.loc.start.line;
3802
+ const lineStart = sourceCode.getIndexFromLoc({ line: ifLine, column: 0 });
3803
+ const lineText = sourceCode.lines[ifLine - 1];
3804
+ const indent = lineText.match(/^\s*/)[0];
3805
+
3806
+ // Insert variable declaration before the if statement
3807
+ fixes.push(fixer.insertTextBeforeRange(
3808
+ [lineStart, lineStart],
3809
+ `const ${varName} = ${groupText};\n${indent}`,
3810
+ ));
3811
+
3812
+ // Replace the nested group with the variable name in the condition
3813
+ // We need to replace including the parentheses around the group
3814
+ const tokenBefore = sourceCode.getTokenBefore(deepGroup.node);
3815
+ const tokenAfter = sourceCode.getTokenAfter(deepGroup.node);
3816
+
3817
+ if (tokenBefore && tokenAfter && tokenBefore.value === "(" && tokenAfter.value === ")") {
3818
+ fixes.push(fixer.replaceTextRange(
3819
+ [tokenBefore.range[0], tokenAfter.range[1]],
3820
+ varName,
3821
+ ));
3822
+ } else {
3823
+ fixes.push(fixer.replaceText(deepGroup.node, varName));
3824
+ }
3825
+
3826
+ return fixes;
3827
+ },
3828
+ message: `Condition nesting depth (${nestingDepth}) exceeds maximum (${maxNestingLevel}). Extract deeply nested condition to a variable.`,
3829
+ node: deepGroup.node,
3830
+ });
3831
+
3832
+ return; // Don't process other rules for this statement
3833
+ }
3834
+ }
3835
+
3612
3836
  const isMultiLine = openParen.loc.start.line !== closeParen.loc.end.line;
3613
3837
 
3614
3838
  // Check if a BinaryExpression is split across lines (left/operator/right not on same line)
@@ -3639,8 +3863,146 @@ const multilineIfConditions = {
3639
3863
  return sourceCode.getText(n);
3640
3864
  };
3641
3865
 
3642
- // maxOperands or fewer operands: keep on single line (operand starts on same line)
3866
+ // Check if any nested parenthesized group has >maxOperands - format multiline inline
3867
+ const nestedGroupExceeding = findNestedGroupExceedingMaxOperandsHandler(test);
3868
+ if (nestedGroupExceeding) {
3869
+ // Get indentation for formatting
3870
+ const lineText = sourceCode.lines[node.loc.start.line - 1];
3871
+ const parenIndent = lineText.match(/^\s*/)[0];
3872
+ const contentIndent = parenIndent + " ";
3873
+
3874
+ // Build multiline text for operands inside a nested group
3875
+ // isOuterGroup=true means we're at the target group itself (ignore its own parentheses)
3876
+ // Also recursively expands any inner nested groups with >maxOperands
3877
+ const buildNestedMultilineHandler = (n, isOuterGroup = false, currentIndent = contentIndent) => {
3878
+ if (n.type === "LogicalExpression" && (isOuterGroup || !isParenthesizedHandler(n))) {
3879
+ const leftText = buildNestedMultilineHandler(n.left, false, currentIndent);
3880
+ const rightText = buildNestedMultilineHandler(n.right, false, currentIndent);
3881
+
3882
+ return `${leftText}\n${currentIndent}${n.operator} ${rightText}`;
3883
+ }
3884
+
3885
+ // Check if this is a parenthesized group with >maxOperands - also expand it
3886
+ if (n.type === "LogicalExpression" && isParenthesizedHandler(n)) {
3887
+ const innerCount = countOperandsInsideGroupHandler(n);
3888
+ if (innerCount > maxOperands) {
3889
+ const innerIndent = currentIndent + " ";
3890
+ const buildInner = (inner) => {
3891
+ if (inner.type === "LogicalExpression" && !isParenthesizedHandler(inner)) {
3892
+ const l = buildInner(inner.left);
3893
+ const r = buildInner(inner.right);
3894
+
3895
+ return `${l}\n${innerIndent}${inner.operator} ${r}`;
3896
+ }
3897
+
3898
+ return getSourceTextWithGroupsHandler(inner);
3899
+ };
3900
+
3901
+ const innerLeft = buildInner(n.left);
3902
+ const innerRight = buildInner(n.right);
3903
+
3904
+ return `(\n${innerIndent}${innerLeft}\n${innerIndent}${n.operator} ${innerRight}\n${currentIndent})`;
3905
+ }
3906
+ }
3907
+
3908
+ return getSourceTextWithGroupsHandler(n);
3909
+ };
3910
+
3911
+ // Build the full condition with nested group formatted multiline
3912
+ const buildFullConditionHandler = (n, targetNode) => {
3913
+ if (n === targetNode) {
3914
+ // This is the nested group - format it multiline
3915
+ // Pass true to ignore outer parentheses check
3916
+ const nestedText = buildNestedMultilineHandler(n, true);
3917
+
3918
+ return `(\n${contentIndent}${nestedText}\n${parenIndent})`;
3919
+ }
3920
+
3921
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
3922
+ const leftText = buildFullConditionHandler(n.left, targetNode);
3923
+ const rightText = buildFullConditionHandler(n.right, targetNode);
3924
+
3925
+ return `${leftText} ${n.operator} ${rightText}`;
3926
+ }
3927
+
3928
+ if (n.type === "LogicalExpression" && isParenthesizedHandler(n)) {
3929
+ // Check if this contains the target
3930
+ const containsTargetHandler = (node, target) => {
3931
+ if (node === target) return true;
3932
+ if (node.type === "LogicalExpression") {
3933
+ return containsTargetHandler(node.left, target) || containsTargetHandler(node.right, target);
3934
+ }
3935
+
3936
+ return false;
3937
+ };
3938
+
3939
+ if (containsTargetHandler(n, targetNode)) {
3940
+ const innerText = buildFullConditionHandler(n, targetNode);
3941
+
3942
+ return `(${innerText})`;
3943
+ }
3944
+ }
3945
+
3946
+ return getSourceTextWithGroupsHandler(n);
3947
+ };
3948
+
3949
+ // Check if already correctly formatted
3950
+ // Collect operands INSIDE the nested group (not treating the group itself as 1 operand)
3951
+ const collectInsideGroupHandler = (node) => {
3952
+ const operands = [];
3953
+ const collectHelper = (n) => {
3954
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
3955
+ collectHelper(n.left);
3956
+ collectHelper(n.right);
3957
+ } else {
3958
+ operands.push(n);
3959
+ }
3960
+ };
3961
+
3962
+ // Start from children directly, ignoring outer parentheses
3963
+ if (node.type === "LogicalExpression") {
3964
+ collectHelper(node.left);
3965
+ collectHelper(node.right);
3966
+ }
3967
+
3968
+ return operands;
3969
+ };
3970
+
3971
+ const nestedOperands = collectInsideGroupHandler(nestedGroupExceeding);
3972
+ const allOnDifferentLines = nestedOperands.every((op, i) => {
3973
+ if (i === 0) return true;
3974
+
3975
+ return op.loc.start.line !== nestedOperands[i - 1].loc.start.line;
3976
+ });
3977
+
3978
+ if (!allOnDifferentLines) {
3979
+ context.report({
3980
+ fix: (fixer) => {
3981
+ const newCondition = buildFullConditionHandler(test, nestedGroupExceeding);
3982
+
3983
+ // Replace just the content between if statement's parens
3984
+ return fixer.replaceTextRange(
3985
+ [openParen.range[1], closeParen.range[0]],
3986
+ newCondition,
3987
+ );
3988
+ },
3989
+ message: `Nested condition with >${maxOperands} operands should be formatted multiline`,
3990
+ node: nestedGroupExceeding,
3991
+ });
3992
+
3993
+ return;
3994
+ }
3995
+ }
3996
+
3997
+ // maxOperands or fewer operands: keep on single line
3998
+ // BUT skip if any nested group has >maxOperands (already expanded or needs expansion)
3643
3999
  if (operands.length <= maxOperands) {
4000
+ // Check if any nested parenthesized group has >maxOperands (should stay multiline)
4001
+ const hasExpandedNestedGroup = findNestedGroupExceedingMaxOperandsHandler(test);
4002
+ if (hasExpandedNestedGroup) {
4003
+ return; // Don't collapse - nested group needs multiline
4004
+ }
4005
+
3644
4006
  // Check if all operands START on the same line
3645
4007
  const firstOperandStartLine = operands[0].loc.start.line;
3646
4008
  const allOperandsStartOnSameLine = operands.every(
@@ -3738,13 +4100,39 @@ const multilineIfConditions = {
3738
4100
  const lineText = sourceCode.lines[node.loc.start.line - 1];
3739
4101
  const parenIndent = lineText.match(/^\s*/)[0];
3740
4102
  const indent = parenIndent + " ";
4103
+ const nestedIndent = indent + " ";
3741
4104
 
3742
- const buildMultilineHandler = (n) => {
4105
+ // Build multiline, also expanding nested groups with >maxOperands
4106
+ const buildMultilineHandler = (n, currentIndent) => {
3743
4107
  if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
3744
- const leftText = buildMultilineHandler(n.left);
3745
- const rightText = buildMultilineHandler(n.right);
4108
+ const leftText = buildMultilineHandler(n.left, currentIndent);
4109
+ const rightText = buildMultilineHandler(n.right, currentIndent);
3746
4110
 
3747
- return `${leftText}\n${indent}${n.operator} ${rightText}`;
4111
+ return `${leftText}\n${currentIndent}${n.operator} ${rightText}`;
4112
+ }
4113
+
4114
+ // Check if this is a parenthesized group with >maxOperands - expand it too
4115
+ if (n.type === "LogicalExpression" && isParenthesizedHandler(n)) {
4116
+ const innerCount = countOperandsInsideGroupHandler(n);
4117
+ if (innerCount > maxOperands) {
4118
+ // Expand this nested group
4119
+ const innerIndent = currentIndent + " ";
4120
+ const buildInnerHandler = (inner) => {
4121
+ if (inner.type === "LogicalExpression" && !isParenthesizedHandler(inner)) {
4122
+ const l = buildInnerHandler(inner.left);
4123
+ const r = buildInnerHandler(inner.right);
4124
+
4125
+ return `${l}\n${innerIndent}${inner.operator} ${r}`;
4126
+ }
4127
+
4128
+ return getSourceTextWithGroupsHandler(inner);
4129
+ };
4130
+
4131
+ const innerLeft = buildInnerHandler(n.left);
4132
+ const innerRight = buildInnerHandler(n.right);
4133
+
4134
+ return `(\n${innerIndent}${innerLeft}\n${innerIndent}${n.operator} ${innerRight}\n${currentIndent})`;
4135
+ }
3748
4136
  }
3749
4137
 
3750
4138
  return getSourceTextWithGroupsHandler(n);
@@ -3752,7 +4140,7 @@ const multilineIfConditions = {
3752
4140
 
3753
4141
  return fixer.replaceTextRange(
3754
4142
  [openParen.range[0], closeParen.range[1]],
3755
- `(\n${indent}${buildMultilineHandler(test)}\n${parenIndent})`,
4143
+ `(\n${indent}${buildMultilineHandler(test, indent)}\n${parenIndent})`,
3756
4144
  );
3757
4145
  },
3758
4146
  message: `If conditions with more than ${maxOperands} operands should be multiline, with each operand on its own line`,
@@ -3804,8 +4192,12 @@ const multilineIfConditions = {
3804
4192
  return sourceCode.getText(n);
3805
4193
  };
3806
4194
 
3807
- // maxOperands or fewer operands: keep on single line (operand starts on same line)
3808
- if (operands.length <= maxOperands) {
4195
+ // Check if any nested parenthesized group has >maxOperands
4196
+ // For properties, if nested group exceeds, skip single-line formatting (let multiline handle it)
4197
+ const nestedGroupExceeding = findNestedGroupExceedingMaxOperandsHandler(value);
4198
+
4199
+ // maxOperands or fewer operands AND no nested group exceeds maxOperands: keep on single line
4200
+ if (operands.length <= maxOperands && !nestedGroupExceeding) {
3809
4201
  // Check if all operands START on the same line
3810
4202
  const firstOperandStartLine = operands[0].loc.start.line;
3811
4203
  const allOperandsStartOnSameLine = operands.every(
@@ -3899,7 +4291,7 @@ const multilineIfConditions = {
3899
4291
  properties: {
3900
4292
  maxOperands: {
3901
4293
  default: 3,
3902
- description: "Maximum operands to keep on single line (default: 3)",
4294
+ description: "Maximum operands to keep on single line (default: 3). Also applies to nested groups.",
3903
4295
  minimum: 1,
3904
4296
  type: "integer",
3905
4297
  },
@@ -3920,6 +4312,10 @@ const multilineIfConditions = {
3920
4312
  * Formats ternary expressions based on condition operand count:
3921
4313
  * - ≤maxOperands (default: 3): Always collapse to single line
3922
4314
  * - >maxOperands: Format multiline with each operand on its own line
4315
+ * - Simple parenthesized nested ternaries (≤maxOperands) count as 1 operand
4316
+ * - Complex nested ternaries (>maxOperands) are skipped (format manually)
4317
+ * - Nested groups with >maxOperands are formatted multiline inline
4318
+ * - Groups exceeding nesting level 2 are extracted to variables
3923
4319
  *
3924
4320
  * Options:
3925
4321
  * { maxOperands: 3 } - Maximum operands to keep on single line (default: 3)
@@ -3927,6 +4323,7 @@ const multilineIfConditions = {
3927
4323
  * ✓ Good (≤3 operands - single line):
3928
4324
  * const x = a && b && c ? "yes" : "no";
3929
4325
  * const url = lang === "ar" ? "/ar/path" : "/en/path";
4326
+ * const inputType = showToggle ? (showPwd ? "text" : "password") : type;
3930
4327
  *
3931
4328
  * ✓ Good (>3 operands - multiline):
3932
4329
  * const x = variant === "ghost"
@@ -3949,6 +4346,7 @@ const ternaryConditionMultiline = {
3949
4346
  const sourceCode = context.sourceCode || context.getSourceCode();
3950
4347
  const options = context.options[0] || {};
3951
4348
  const maxOperands = options.maxOperands ?? 3;
4349
+ const maxNestingLevel = 2; // Fixed at 2 - not configurable to avoid overly complex conditions
3952
4350
 
3953
4351
  // Check if node is wrapped in parentheses
3954
4352
  const isParenthesizedHandler = (node) => {
@@ -3995,6 +4393,157 @@ const ternaryConditionMultiline = {
3995
4393
  return operands;
3996
4394
  };
3997
4395
 
4396
+ // Calculate max nesting depth of parenthesized groups in a logical expression
4397
+ // Level 0: a && b (no groups)
4398
+ // Level 1: (a && b) || c (one group at top)
4399
+ // Level 2: (a && (b || c)) || d (group inside group)
4400
+ const getNestingDepthHandler = (node, currentDepth = 0) => {
4401
+ if (node.type !== "LogicalExpression") return currentDepth;
4402
+
4403
+ let maxDepth = currentDepth;
4404
+
4405
+ // Check left side
4406
+ if (node.left.type === "LogicalExpression") {
4407
+ const leftDepth = isParenthesizedHandler(node.left)
4408
+ ? getNestingDepthHandler(node.left, currentDepth + 1)
4409
+ : getNestingDepthHandler(node.left, currentDepth);
4410
+ maxDepth = Math.max(maxDepth, leftDepth);
4411
+ }
4412
+
4413
+ // Check right side
4414
+ if (node.right.type === "LogicalExpression") {
4415
+ const rightDepth = isParenthesizedHandler(node.right)
4416
+ ? getNestingDepthHandler(node.right, currentDepth + 1)
4417
+ : getNestingDepthHandler(node.right, currentDepth);
4418
+ maxDepth = Math.max(maxDepth, rightDepth);
4419
+ }
4420
+
4421
+ return maxDepth;
4422
+ };
4423
+
4424
+ // Find the deepest nested parenthesized group that exceeds maxNestingLevel
4425
+ const findDeepNestedGroupHandler = (node, currentDepth = 0) => {
4426
+ if (node.type !== "LogicalExpression") return null;
4427
+
4428
+ let deepestGroup = null;
4429
+
4430
+ // Check left side
4431
+ if (node.left.type === "LogicalExpression") {
4432
+ const leftIsParen = isParenthesizedHandler(node.left);
4433
+ const newDepth = leftIsParen ? currentDepth + 1 : currentDepth;
4434
+
4435
+ if (leftIsParen && newDepth > maxNestingLevel) {
4436
+ // This group exceeds max nesting
4437
+ deepestGroup = {
4438
+ node: node.left,
4439
+ depth: newDepth,
4440
+ parent: node,
4441
+ side: "left",
4442
+ };
4443
+ }
4444
+
4445
+ // Recurse to find even deeper
4446
+ const deeper = findDeepNestedGroupHandler(node.left, newDepth);
4447
+ if (deeper && (!deepestGroup || deeper.depth > deepestGroup.depth)) {
4448
+ deepestGroup = deeper;
4449
+ }
4450
+ }
4451
+
4452
+ // Check right side
4453
+ if (node.right.type === "LogicalExpression") {
4454
+ const rightIsParen = isParenthesizedHandler(node.right);
4455
+ const newDepth = rightIsParen ? currentDepth + 1 : currentDepth;
4456
+
4457
+ if (rightIsParen && newDepth > maxNestingLevel) {
4458
+ // This group exceeds max nesting
4459
+ const rightGroup = {
4460
+ node: node.right,
4461
+ depth: newDepth,
4462
+ parent: node,
4463
+ side: "right",
4464
+ };
4465
+ if (!deepestGroup || rightGroup.depth > deepestGroup.depth) {
4466
+ deepestGroup = rightGroup;
4467
+ }
4468
+ }
4469
+
4470
+ // Recurse to find even deeper
4471
+ const deeper = findDeepNestedGroupHandler(node.right, newDepth);
4472
+ if (deeper && (!deepestGroup || deeper.depth > deepestGroup.depth)) {
4473
+ deepestGroup = deeper;
4474
+ }
4475
+ }
4476
+
4477
+ return deepestGroup;
4478
+ };
4479
+
4480
+ // Generate descriptive variable name based on condition structure
4481
+ const getConditionNameHandler = (n) => {
4482
+ if (n.type === "LogicalExpression") {
4483
+ const leftName = getConditionNameHandler(n.left);
4484
+ const rightName = getConditionNameHandler(n.right);
4485
+ const opName = n.operator === "&&" ? "And" : "Or";
4486
+
4487
+ return `${leftName}${opName}${rightName}`;
4488
+ }
4489
+ if (n.type === "Identifier") {
4490
+ return n.name.charAt(0).toUpperCase() + n.name.slice(1);
4491
+ }
4492
+ if (n.type === "BinaryExpression" || n.type === "CallExpression" || n.type === "MemberExpression") {
4493
+ return "Expr";
4494
+ }
4495
+
4496
+ return "Cond";
4497
+ };
4498
+
4499
+ // Count operands inside a group, ignoring the outer parentheses of the group itself
4500
+ // This is used to count operands INSIDE a parenthesized group
4501
+ const countOperandsInsideGroupHandler = (node) => {
4502
+ if (node.type !== "LogicalExpression") return 1;
4503
+
4504
+ let count = 0;
4505
+ const countHelperHandler = (n) => {
4506
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
4507
+ countHelperHandler(n.left);
4508
+ countHelperHandler(n.right);
4509
+ } else {
4510
+ count += 1;
4511
+ }
4512
+ };
4513
+
4514
+ // Start by recursing into children directly (ignoring outer parens)
4515
+ countHelperHandler(node.left);
4516
+ countHelperHandler(node.right);
4517
+
4518
+ return count;
4519
+ };
4520
+
4521
+ // Find a nested parenthesized group that has >maxOperands operands
4522
+ // Returns the first such group found (for extraction to variable)
4523
+ const findNestedGroupExceedingMaxOperandsHandler = (node) => {
4524
+ if (node.type !== "LogicalExpression") return null;
4525
+
4526
+ // Check if this node is a parenthesized group with >maxOperands
4527
+ if (isParenthesizedHandler(node)) {
4528
+ const insideCount = countOperandsInsideGroupHandler(node);
4529
+ if (insideCount > maxOperands) {
4530
+ return node;
4531
+ }
4532
+ }
4533
+
4534
+ // Recursively check children
4535
+ if (node.left.type === "LogicalExpression") {
4536
+ const found = findNestedGroupExceedingMaxOperandsHandler(node.left);
4537
+ if (found) return found;
4538
+ }
4539
+ if (node.right.type === "LogicalExpression") {
4540
+ const found = findNestedGroupExceedingMaxOperandsHandler(node.right);
4541
+ if (found) return found;
4542
+ }
4543
+
4544
+ return null;
4545
+ };
4546
+
3998
4547
  // Check if a BinaryExpression is split across lines
3999
4548
  const isBinaryExpressionSplitHandler = (n) => {
4000
4549
  if (n.type !== "BinaryExpression") return false;
@@ -4056,18 +4605,22 @@ const ternaryConditionMultiline = {
4056
4605
  if (test.type === "CallExpression") return true;
4057
4606
 
4058
4607
  // Logical expression with ≤maxOperands is still simple
4608
+ // BUT if any nested group has >maxOperands, it's not simple
4059
4609
  if (test.type === "LogicalExpression") {
4060
- return collectOperandsHandler(test).length <= maxOperands;
4610
+ const operandCount = collectOperandsHandler(test).length;
4611
+ const nestedGroupExceeding = findNestedGroupExceedingMaxOperandsHandler(test);
4612
+ return operandCount <= maxOperands && !nestedGroupExceeding;
4061
4613
  }
4062
4614
 
4063
4615
  return false;
4064
4616
  };
4065
4617
 
4066
- // Get the full ternary as single line text
4618
+ // Get the full ternary as single line text (preserving parentheses around nested ternaries)
4067
4619
  const getTernarySingleLineHandler = (node) => {
4068
4620
  const testText = sourceCode.getText(node.test).replace(/\s+/g, " ").trim();
4069
- const consequentText = sourceCode.getText(node.consequent).replace(/\s+/g, " ").trim();
4070
- const alternateText = sourceCode.getText(node.alternate).replace(/\s+/g, " ").trim();
4621
+ // Use getSourceTextWithGroupsHandler to preserve parentheses around nested expressions
4622
+ const consequentText = getSourceTextWithGroupsHandler(node.consequent).replace(/\s+/g, " ").trim();
4623
+ const alternateText = getSourceTextWithGroupsHandler(node.alternate).replace(/\s+/g, " ").trim();
4071
4624
 
4072
4625
  return `${testText} ? ${consequentText} : ${alternateText}`;
4073
4626
  };
@@ -4088,6 +4641,21 @@ const ternaryConditionMultiline = {
4088
4641
  return false;
4089
4642
  };
4090
4643
 
4644
+ // Check if a nested ternary has complex condition (>maxOperands)
4645
+ const hasComplexNestedTernaryHandler = (node) => {
4646
+ const checkBranch = (branch) => {
4647
+ if (branch.type === "ConditionalExpression" && isParenthesizedHandler(branch)) {
4648
+ const nestedOperands = collectOperandsHandler(branch.test);
4649
+
4650
+ return nestedOperands.length > maxOperands;
4651
+ }
4652
+
4653
+ return false;
4654
+ };
4655
+
4656
+ return checkBranch(node.consequent) || checkBranch(node.alternate);
4657
+ };
4658
+
4091
4659
  // Check if ? or : is on its own line without its value
4092
4660
  const isOperatorOnOwnLineHandler = (node) => {
4093
4661
  const questionToken = sourceCode.getTokenAfter(node.test, (t) => t.value === "?");
@@ -4106,16 +4674,16 @@ const ternaryConditionMultiline = {
4106
4674
  return false;
4107
4675
  };
4108
4676
 
4109
- // Handle simple ternaries (≤maxOperands) - always collapse to single line
4677
+ // Handle simple ternaries (≤maxOperands) - collapse or format with complex nested
4110
4678
  const handleSimpleTernaryHandler = (node) => {
4111
4679
  const isOnSingleLine = node.loc.start.line === node.loc.end.line;
4112
4680
  const hasOperatorOnOwnLine = isOperatorOnOwnLineHandler(node);
4113
4681
 
4114
- // Skip if already on single line and no formatting issues
4115
- if (isOnSingleLine && !hasOperatorOnOwnLine) return false;
4682
+ // Skip unparenthesized nested ternaries (parenthesized ones count as 1 operand)
4683
+ const hasUnparenthesizedNestedTernary = (node.consequent.type === "ConditionalExpression" && !isParenthesizedHandler(node.consequent))
4684
+ || (node.alternate.type === "ConditionalExpression" && !isParenthesizedHandler(node.alternate));
4116
4685
 
4117
- // Skip nested ternaries
4118
- if (node.consequent.type === "ConditionalExpression" || node.alternate.type === "ConditionalExpression") {
4686
+ if (hasUnparenthesizedNestedTernary) {
4119
4687
  return false;
4120
4688
  }
4121
4689
 
@@ -4124,6 +4692,15 @@ const ternaryConditionMultiline = {
4124
4692
  return false;
4125
4693
  }
4126
4694
 
4695
+ // Skip parenthesized nested ternaries with complex condition (>maxOperands)
4696
+ // These should be formatted manually or stay as-is
4697
+ if (hasComplexNestedTernaryHandler(node)) {
4698
+ return false;
4699
+ }
4700
+
4701
+ // Skip if already on single line and no formatting issues
4702
+ if (isOnSingleLine && !hasOperatorOnOwnLine) return false;
4703
+
4127
4704
  // Calculate what the single line would look like
4128
4705
  const singleLineText = getTernarySingleLineHandler(node);
4129
4706
 
@@ -4145,15 +4722,159 @@ const ternaryConditionMultiline = {
4145
4722
  const testEndLine = test.loc.end.line;
4146
4723
  const isMultiLine = testStartLine !== testEndLine;
4147
4724
 
4148
- // ≤maxOperands operands: always collapse to single line (regardless of line length)
4725
+ // Check if any nested parenthesized group has >maxOperands - format multiline inline
4726
+ const nestedGroupExceeding = findNestedGroupExceedingMaxOperandsHandler(test);
4727
+ if (nestedGroupExceeding) {
4728
+ // Get indentation for formatting
4729
+ const parent = node.parent;
4730
+ let parenIndent = "";
4731
+
4732
+ if (parent && parent.loc) {
4733
+ const parentLineText = sourceCode.lines[parent.loc.start.line - 1];
4734
+ parenIndent = parentLineText.match(/^\s*/)[0];
4735
+ }
4736
+
4737
+ const contentIndent = parenIndent + " ";
4738
+
4739
+ // Build multiline text for operands inside a nested group
4740
+ // isOuterGroup=true means we're at the target group itself (ignore its own parentheses)
4741
+ // Also recursively expands any inner nested groups with >maxOperands
4742
+ const buildNestedMultilineHandler = (n, isOuterGroup = false, currentIndent = contentIndent) => {
4743
+ if (n.type === "LogicalExpression" && (isOuterGroup || !isParenthesizedHandler(n))) {
4744
+ const leftText = buildNestedMultilineHandler(n.left, false, currentIndent);
4745
+ const rightText = buildNestedMultilineHandler(n.right, false, currentIndent);
4746
+
4747
+ return `${leftText}\n${currentIndent}${n.operator} ${rightText}`;
4748
+ }
4749
+
4750
+ // Check if this is a parenthesized group with >maxOperands - also expand it
4751
+ if (n.type === "LogicalExpression" && isParenthesizedHandler(n)) {
4752
+ const innerCount = countOperandsInsideGroupHandler(n);
4753
+ if (innerCount > maxOperands) {
4754
+ const innerIndent = currentIndent + " ";
4755
+ const buildInner = (inner) => {
4756
+ if (inner.type === "LogicalExpression" && !isParenthesizedHandler(inner)) {
4757
+ const l = buildInner(inner.left);
4758
+ const r = buildInner(inner.right);
4759
+
4760
+ return `${l}\n${innerIndent}${inner.operator} ${r}`;
4761
+ }
4762
+
4763
+ return getSourceTextWithGroupsHandler(inner);
4764
+ };
4765
+
4766
+ const innerLeft = buildInner(n.left);
4767
+ const innerRight = buildInner(n.right);
4768
+
4769
+ return `(\n${innerIndent}${innerLeft}\n${innerIndent}${n.operator} ${innerRight}\n${currentIndent})`;
4770
+ }
4771
+ }
4772
+
4773
+ return getSourceTextWithGroupsHandler(n);
4774
+ };
4775
+
4776
+ // Build the full condition with nested group formatted multiline
4777
+ const buildFullConditionHandler = (n, targetNode) => {
4778
+ if (n === targetNode) {
4779
+ // This is the nested group - format it multiline
4780
+ // Pass true to ignore outer parentheses check
4781
+ const nestedText = buildNestedMultilineHandler(n, true);
4782
+
4783
+ return `(\n${contentIndent}${nestedText}\n${parenIndent})`;
4784
+ }
4785
+
4786
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
4787
+ const leftText = buildFullConditionHandler(n.left, targetNode);
4788
+ const rightText = buildFullConditionHandler(n.right, targetNode);
4789
+
4790
+ return `${leftText} ${n.operator} ${rightText}`;
4791
+ }
4792
+
4793
+ if (n.type === "LogicalExpression" && isParenthesizedHandler(n)) {
4794
+ // Check if this contains the target
4795
+ const containsTargetHandler = (node, target) => {
4796
+ if (node === target) return true;
4797
+ if (node.type === "LogicalExpression") {
4798
+ return containsTargetHandler(node.left, target) || containsTargetHandler(node.right, target);
4799
+ }
4800
+
4801
+ return false;
4802
+ };
4803
+
4804
+ if (containsTargetHandler(n, targetNode)) {
4805
+ const innerText = buildFullConditionHandler(n, targetNode);
4806
+
4807
+ return `(${innerText})`;
4808
+ }
4809
+ }
4810
+
4811
+ return getSourceTextWithGroupsHandler(n);
4812
+ };
4813
+
4814
+ // Check if already correctly formatted
4815
+ // Collect operands INSIDE the nested group (not treating the group itself as 1 operand)
4816
+ const collectInsideGroupHandler = (node) => {
4817
+ const operands = [];
4818
+ const collectHelper = (n) => {
4819
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
4820
+ collectHelper(n.left);
4821
+ collectHelper(n.right);
4822
+ } else {
4823
+ operands.push(n);
4824
+ }
4825
+ };
4826
+
4827
+ // Start from children directly, ignoring outer parentheses
4828
+ if (node.type === "LogicalExpression") {
4829
+ collectHelper(node.left);
4830
+ collectHelper(node.right);
4831
+ }
4832
+
4833
+ return operands;
4834
+ };
4835
+
4836
+ const nestedOperands = collectInsideGroupHandler(nestedGroupExceeding);
4837
+ const allOnDifferentLines = nestedOperands.every((op, i) => {
4838
+ if (i === 0) return true;
4839
+
4840
+ return op.loc.start.line !== nestedOperands[i - 1].loc.start.line;
4841
+ });
4842
+
4843
+ if (!allOnDifferentLines) {
4844
+ const newCondition = buildFullConditionHandler(test, nestedGroupExceeding);
4845
+
4846
+ // Get consequent and alternate text
4847
+ const consequentText = getSourceTextWithGroupsHandler(node.consequent).replace(/\s+/g, " ").trim();
4848
+ const alternateText = getSourceTextWithGroupsHandler(node.alternate).replace(/\s+/g, " ").trim();
4849
+
4850
+ context.report({
4851
+ fix: (fixer) => fixer.replaceText(node, `${newCondition} ? ${consequentText} : ${alternateText}`),
4852
+ message: `Nested condition with >${maxOperands} operands should be formatted multiline`,
4853
+ node: nestedGroupExceeding,
4854
+ });
4855
+
4856
+ return;
4857
+ }
4858
+ }
4859
+
4860
+ // ≤maxOperands operands: collapse to single line
4861
+ // BUT skip if any nested group has >maxOperands (already expanded or needs expansion)
4149
4862
  if (operands.length <= maxOperands) {
4863
+ // Check if any nested parenthesized group has >maxOperands (should stay multiline)
4864
+ const hasExpandedNestedGroup = findNestedGroupExceedingMaxOperandsHandler(test);
4865
+ if (hasExpandedNestedGroup) {
4866
+ return; // Don't collapse - nested group needs multiline
4867
+ }
4868
+
4150
4869
  // Skip if branches have complex objects
4151
4870
  const hasComplexBranches = hasComplexObjectHandler(node.consequent) || hasComplexObjectHandler(node.alternate);
4152
4871
 
4153
- // Skip nested ternaries
4154
- const hasNestedTernary = node.consequent.type === "ConditionalExpression" || node.alternate.type === "ConditionalExpression";
4872
+ // Skip unparenthesized nested ternaries (parenthesized ones count as 1 operand)
4873
+ const hasUnparenthesizedNestedTernary = (node.consequent.type === "ConditionalExpression" && !isParenthesizedHandler(node.consequent))
4874
+ || (node.alternate.type === "ConditionalExpression" && !isParenthesizedHandler(node.alternate));
4155
4875
 
4156
- if (hasComplexBranches || hasNestedTernary) {
4876
+ // Skip parenthesized nested ternaries with complex condition (>maxOperands)
4877
+ if (hasComplexBranches || hasUnparenthesizedNestedTernary || hasComplexNestedTernaryHandler(node)) {
4157
4878
  return;
4158
4879
  }
4159
4880
 
@@ -4312,10 +5033,99 @@ const ternaryConditionMultiline = {
4312
5033
  }
4313
5034
  };
4314
5035
 
5036
+ // Check if this ternary is a nested ternary inside another ternary's branch
5037
+ const isNestedInTernaryBranchHandler = (node) => {
5038
+ let parent = node.parent;
5039
+
5040
+ // Walk up through parentheses
5041
+ while (parent && parent.type === "ParenthesizedExpression") {
5042
+ parent = parent.parent;
5043
+ }
5044
+
5045
+ // Check if parent is a ternary and this node is in consequent or alternate
5046
+ if (parent && parent.type === "ConditionalExpression") {
5047
+ return parent.consequent === node || parent.alternate === node
5048
+ || (node.parent.type === "ParenthesizedExpression"
5049
+ && (parent.consequent === node.parent || parent.alternate === node.parent));
5050
+ }
5051
+
5052
+ return false;
5053
+ };
5054
+
4315
5055
  return {
4316
5056
  ConditionalExpression(node) {
4317
5057
  const { test } = node;
4318
5058
 
5059
+ // Skip nested ternaries that are inside another ternary's branch
5060
+ // They will be formatted by their parent ternary
5061
+ if (isNestedInTernaryBranchHandler(node)) {
5062
+ return;
5063
+ }
5064
+
5065
+ // Check for excessive nesting depth in the condition
5066
+ if (test.type === "LogicalExpression") {
5067
+ const nestingDepth = getNestingDepthHandler(test);
5068
+
5069
+ if (nestingDepth > maxNestingLevel) {
5070
+ const deepGroup = findDeepNestedGroupHandler(test);
5071
+
5072
+ if (deepGroup) {
5073
+ const groupText = getSourceTextWithGroupsHandler(deepGroup.node);
5074
+
5075
+ // Generate unique variable name
5076
+ let varName = `is${getConditionNameHandler(deepGroup.node)}`;
5077
+ // Limit length and sanitize
5078
+ if (varName.length > 30) {
5079
+ varName = "isNestedCondition";
5080
+ }
5081
+
5082
+ context.report({
5083
+ fix: (fixer) => {
5084
+ const fixes = [];
5085
+
5086
+ // Get the line before the ternary statement for inserting the variable
5087
+ // Find the statement that contains this ternary
5088
+ let statementNode = node;
5089
+ while (statementNode.parent && statementNode.parent.type !== "Program" && statementNode.parent.type !== "BlockStatement") {
5090
+ statementNode = statementNode.parent;
5091
+ }
5092
+
5093
+ const ternaryLine = statementNode.loc.start.line;
5094
+ const lineStart = sourceCode.getIndexFromLoc({ line: ternaryLine, column: 0 });
5095
+ const lineText = sourceCode.lines[ternaryLine - 1];
5096
+ const indent = lineText.match(/^\s*/)[0];
5097
+
5098
+ // Insert variable declaration before the statement containing the ternary
5099
+ fixes.push(fixer.insertTextBeforeRange(
5100
+ [lineStart, lineStart],
5101
+ `const ${varName} = ${groupText};\n${indent}`,
5102
+ ));
5103
+
5104
+ // Replace the nested group with the variable name in the condition
5105
+ // We need to replace including the parentheses around the group
5106
+ const tokenBefore = sourceCode.getTokenBefore(deepGroup.node);
5107
+ const tokenAfter = sourceCode.getTokenAfter(deepGroup.node);
5108
+
5109
+ if (tokenBefore && tokenAfter && tokenBefore.value === "(" && tokenAfter.value === ")") {
5110
+ fixes.push(fixer.replaceTextRange(
5111
+ [tokenBefore.range[0], tokenAfter.range[1]],
5112
+ varName,
5113
+ ));
5114
+ } else {
5115
+ fixes.push(fixer.replaceText(deepGroup.node, varName));
5116
+ }
5117
+
5118
+ return fixes;
5119
+ },
5120
+ message: `Ternary condition nesting depth (${nestingDepth}) exceeds maximum (${maxNestingLevel}). Extract deeply nested condition to a variable.`,
5121
+ node: deepGroup.node,
5122
+ });
5123
+
5124
+ return; // Don't process other rules for this statement
5125
+ }
5126
+ }
5127
+ }
5128
+
4319
5129
  // First, try to collapse simple ternaries to single line
4320
5130
  if (isSimpleConditionHandler(test)) {
4321
5131
  if (handleSimpleTernaryHandler(node)) return;
@@ -4337,7 +5147,7 @@ const ternaryConditionMultiline = {
4337
5147
  properties: {
4338
5148
  maxOperands: {
4339
5149
  default: 3,
4340
- description: "Maximum condition operands to keep ternary on single line (default: 3). Ternaries with more operands are formatted multiline.",
5150
+ description: "Maximum condition operands to keep ternary on single line (default: 3). Also applies to nested groups.",
4341
5151
  minimum: 1,
4342
5152
  type: "integer",
4343
5153
  },
@@ -4495,6 +5305,40 @@ const classNamingConvention = {
4495
5305
  create(context) {
4496
5306
  const sourceCode = context.sourceCode || context.getSourceCode();
4497
5307
 
5308
+ // Store classes that need renaming and their references
5309
+ const classesToRename = new Map();
5310
+
5311
+ // Collect all Identifier nodes that might be class references
5312
+ const collectReferencesHandler = (programNode, className) => {
5313
+ const references = [];
5314
+
5315
+ const visitHandler = (node) => {
5316
+ if (!node || typeof node !== "object") return;
5317
+
5318
+ // Found a matching Identifier
5319
+ if (node.type === "Identifier" && node.name === className) {
5320
+ references.push(node);
5321
+ }
5322
+
5323
+ // Recursively visit children
5324
+ for (const key in node) {
5325
+ if (key === "parent" || key === "range" || key === "loc") continue;
5326
+
5327
+ const child = node[key];
5328
+
5329
+ if (Array.isArray(child)) {
5330
+ child.forEach((c) => visitHandler(c));
5331
+ } else if (child && typeof child === "object" && child.type) {
5332
+ visitHandler(child);
5333
+ }
5334
+ }
5335
+ };
5336
+
5337
+ visitHandler(programNode);
5338
+
5339
+ return references;
5340
+ };
5341
+
4498
5342
  return {
4499
5343
  ClassDeclaration(node) {
4500
5344
  if (!node.id || !node.id.name) return;
@@ -4502,31 +5346,33 @@ const classNamingConvention = {
4502
5346
  const className = node.id.name;
4503
5347
 
4504
5348
  if (!className.endsWith("Class")) {
5349
+ classesToRename.set(className, {
5350
+ classIdNode: node.id,
5351
+ newName: `${className}Class`,
5352
+ });
5353
+ }
5354
+ },
5355
+
5356
+ "Program:exit"(programNode) {
5357
+ // Process all classes that need renaming
5358
+ classesToRename.forEach(({ classIdNode, newName }, className) => {
5359
+ // Find all references to this class in the entire program
5360
+ const allReferences = collectReferencesHandler(programNode, className);
5361
+
4505
5362
  context.report({
4506
5363
  fix: (fixer) => {
4507
- const newName = `${className}Class`;
4508
-
4509
- // Find all references to this class and rename them
4510
- const scope = context.sourceCode.getScope
4511
- ? context.sourceCode.getScope(node)
4512
- : context.getScope();
4513
- const variable = scope.set.get(className);
4514
- const fixes = [fixer.replaceText(node.id, newName)];
5364
+ const fixes = [];
4515
5365
 
4516
- if (variable && variable.references) {
4517
- variable.references.forEach((ref) => {
4518
- if (ref.identifier !== node.id) {
4519
- fixes.push(fixer.replaceText(ref.identifier, newName));
4520
- }
4521
- });
4522
- }
5366
+ allReferences.forEach((ref) => {
5367
+ fixes.push(fixer.replaceText(ref, newName));
5368
+ });
4523
5369
 
4524
5370
  return fixes;
4525
5371
  },
4526
5372
  message: `Class name "${className}" should end with "Class" suffix`,
4527
- node: node.id,
5373
+ node: classIdNode,
4528
5374
  });
4529
- }
5375
+ });
4530
5376
  },
4531
5377
  };
4532
5378
  },
@@ -11889,6 +12735,7 @@ const openingBracketsSameLine = {
11889
12735
 
11890
12736
  // Check if a condition expression is split incorrectly across lines
11891
12737
  // (operator and operands should be on the same line)
12738
+ // BUT skip if it's intentionally formatted multiline (operator at start of line pattern)
11892
12739
  const checkConditionSplitHandler = (conditionNode) => {
11893
12740
  // Handle LogicalExpression (&&, ||) and BinaryExpression (===, !==, etc.)
11894
12741
  if (conditionNode.type !== "LogicalExpression" && conditionNode.type !== "BinaryExpression") {
@@ -11899,19 +12746,31 @@ const openingBracketsSameLine = {
11899
12746
 
11900
12747
  // Check if this expression spans multiple lines
11901
12748
  if (conditionNode.loc.start.line !== conditionNode.loc.end.line) {
11902
- // For BinaryExpression, always collapse if split
11903
- // For LogicalExpression, check if operator is on different line
12749
+ // Check if this is intentionally formatted multiline
12750
+ // (pattern: operator at start of line, e.g., multiline-if-conditions format)
11904
12751
  const condOperator = sourceCode.getTokenAfter(
11905
12752
  left,
11906
12753
  (t) => ["&&", "||", "===", "!==", "==", "!=", ">", "<", ">=", "<="].includes(t.value),
11907
12754
  );
11908
12755
 
11909
12756
  if (condOperator) {
12757
+ // Check for intentional multiline format: operator at start of line with proper indentation
12758
+ // This pattern indicates multiline-if-conditions has formatted it this way
12759
+ const textBetween = sourceCode.text.slice(left.range[1], condOperator.range[0]);
12760
+ const hasNewlineBeforeOperator = textBetween.includes("\n");
12761
+ const operatorIsAtLineStart = hasNewlineBeforeOperator
12762
+ && /\n\s*$/.test(textBetween);
12763
+
12764
+ // Skip collapsing if operator is intentionally at the start of a line
12765
+ if (operatorIsAtLineStart) {
12766
+ return false;
12767
+ }
12768
+
11910
12769
  const leftEndLine = left.loc.end.line;
11911
12770
  const operatorLine = condOperator.loc.start.line;
11912
12771
  const rightStartLine = right.loc.start.line;
11913
12772
 
11914
- // Bad: operator or operands on different lines
12773
+ // Bad: operator or operands on different lines (not intentional format)
11915
12774
  if (leftEndLine !== operatorLine || operatorLine !== rightStartLine) {
11916
12775
  const tokenBefore = sourceCode.getTokenBefore(conditionNode);
11917
12776
  const tokenAfter = sourceCode.getTokenAfter(conditionNode);
@@ -12282,6 +13141,9 @@ const openingBracketsSameLine = {
12282
13141
  // Skip if this is the test of an IfStatement - those are handled by multiline-if-conditions
12283
13142
  if (node.parent && node.parent.type === "IfStatement" && node.parent.test === node) return;
12284
13143
 
13144
+ // Skip if this is the test of a ConditionalExpression (ternary) - those are handled by ternary-condition-multiline
13145
+ if (node.parent && node.parent.type === "ConditionalExpression" && node.parent.test === node) return;
13146
+
12285
13147
  // Check if operands are valid for same-line formatting
12286
13148
  const isValidOperand = (n) => n.type === "CallExpression"
12287
13149
  || n.type === "LogicalExpression"
@@ -12319,6 +13181,14 @@ const openingBracketsSameLine = {
12319
13181
  // Correct: ) || canDoHandler( (all on same line)
12320
13182
  // Wrong: ) ||\n canDoHandler( or )\n || canDoHandler(
12321
13183
  if (leftEndLine !== operatorLine || operatorLine !== rightStartLine) {
13184
+ // Check for intentional multiline format (operator at start of line)
13185
+ // This indicates multiline-if-conditions or ternary-condition-multiline has formatted it this way
13186
+ const textBetween = sourceCode.text.slice(left.range[1], operatorToken.range[0]);
13187
+ const operatorIsAtLineStart = textBetween.includes("\n") && /\n\s*$/.test(textBetween);
13188
+
13189
+ // Skip if intentionally formatted with operator at start of line
13190
+ if (operatorIsAtLineStart) return;
13191
+
12322
13192
  // Get the end of left operand (might end with closing paren)
12323
13193
  const leftEndToken = sourceCode.getLastToken(left);
12324
13194