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.
- package/CHANGELOG.md +24 -0
- package/README.md +80 -2
- package/index.js +924 -54
- 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
|
-
//
|
|
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
|
-
*
|
|
3528
|
-
*
|
|
3529
|
-
*
|
|
3530
|
-
*
|
|
3531
|
-
*
|
|
3532
|
-
*
|
|
3533
|
-
*
|
|
3534
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
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${
|
|
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
|
-
//
|
|
3808
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
4070
|
-
const
|
|
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) -
|
|
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
|
|
4115
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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).
|
|
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
|
|
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
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
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:
|
|
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
|
-
//
|
|
11903
|
-
//
|
|
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
|
|