@@ -6,14 +6,15 @@ import type {
6
6
} from 'estree'
7
7
import type { Rule } from 'eslint'
8
8
9
+ import { getNodeContent } from './get-node-content'
9
10
import { toggleNegation } from './toggle-negation'
10
11
import { getSourceCode } from './get-source-code'
11
12
import { isConjunction } from './is-conjunction'
12
13
import { isDisjunction } from './is-disjunction'
13
14
14
15
interface TransformOptions {
15
16
/** The type of logical expression to transform. */
16
- expressionType : 'conjunction' | 'disjunction'
17
+ expressionType : ExpressionType
17
18
/** Whether the transformed expression should be wrapped in parentheses. */
18
19
shouldWrapInParens : boolean
19
20
/** The ESLint rule context. */
@@ -22,6 +23,41 @@ interface TransformOptions {
22
23
node : UnaryExpression
23
24
}
24
25
26
+ interface TransformWithFormattingOptions {
27
+ /** The source logical operator. */
28
+ sourceOperator : LogicalOperator
29
+ /** The target logical operator. */
30
+ targetOperator : LogicalOperator
31
+ /** The logical expression to transform. */
32
+ expression : LogicalExpression
33
+ /** The ESLint rule context. */
34
+ context : Rule . RuleContext
35
+ }
36
+
37
+ interface TransformSimpleOptions {
38
+ /** The target logical operator. */
39
+ targetOperator : LogicalOperator
40
+ /** The type of logical expression. */
41
+ expressionType : ExpressionType
42
+ /** The logical expression to transform. */
43
+ expression : LogicalExpression
44
+ /** The ESLint rule context. */
45
+ context : Rule . RuleContext
46
+ }
47
+
48
+ interface FlattenOperandsOptions {
49
+ /** The type of logical expression. */
50
+ expressionType : ExpressionType
51
+ /** The ESLint rule context. */
52
+ context : Rule . RuleContext
53
+ /** The logical expression to flatten. */
54
+ expression : Expression
55
+ /** The current recursion depth. */
56
+ depth ?: number
57
+ }
58
+
59
+ type ExpressionType = 'conjunction' | 'disjunction'
60
+
25
61
const MAX_DEPTH = 10
26
62
27
63
const OPERATOR_MAPPING : Partial < Record < LogicalOperator , LogicalOperator > > = {
@@ -31,25 +67,121 @@ const OPERATOR_MAPPING: Partial<Record<LogicalOperator, LogicalOperator>> = {
31
67
32
68
/**
33
69
* Checks if the expression matches the specified logical type.
34
- * @param {Expression } expr - The expression to check.
35
- * @param {'conjunction' | 'disjunction' } type - The type to check against.
70
+ * @param {Expression } expression - The expression to check.
71
+ * @param {ExpressionType } type - The type to check against.
36
72
* @returns {boolean } True if the expression matches the type, false otherwise.
37
73
*/
38
74
let matchesExpressionType = (
39
- expr : Expression ,
40
- type : 'conjunction' | 'disjunction' ,
41
- ) : boolean => {
42
- if ( type === 'conjunction' ) {
43
- return isConjunction ( expr )
75
+ expression : Expression ,
76
+ type : ExpressionType ,
77
+ ) : boolean =>
78
+ type === 'conjunction' ? isConjunction ( expression ) : isDisjunction ( expression )
79
+
80
+ /**
81
+ * Checks if the text contains special formatting like comments or multiple
82
+ * spaces.
83
+ * @param {string } text - The text to check.
84
+ * @returns {boolean } True if the text contains special formatting.
85
+ */
86
+ let hasSpecialFormatting = ( text : string ) : boolean =>
87
+ text . includes ( '//' ) ||
88
+ text . includes ( '/*' ) ||
89
+ text . includes ( '\n' ) ||
90
+ / \s { 2 , } / u. test ( text )
91
+
92
+ /**
93
+ * Transforms an expression with special formatting (comments, multiple spaces).
94
+ * @param {TransformWithFormattingOptions } options - The transformation options.
95
+ * @returns {string } The transformed expression with preserved formatting.
96
+ */
97
+ let transformWithFormatting = ( {
98
+ sourceOperator,
99
+ targetOperator,
100
+ expression,
101
+ context,
102
+ } : TransformWithFormattingOptions ) : string => {
103
+ let sourceCode = getSourceCode ( context )
104
+
105
+ let leftText = toggleNegation ( expression . left , context )
106
+ let rightText = toggleNegation ( expression . right , context )
107
+
108
+ if ( ! expression . left . range || ! expression . right . range ) {
109
+ return `${ leftText } ${ targetOperator } ${ rightText } `
110
+ }
111
+
112
+ let [ , leftEnd ] = expression . left . range
113
+ let [ rightStart ] = expression . right . range
114
+ let textBetween = sourceCode . text . slice ( leftEnd , rightStart )
115
+
116
+ let formattedOperator = textBetween . replaceAll (
117
+ new RegExp ( sourceOperator . replaceAll ( / [ $ ( ) * + . ? [ \\ \] ^ { | } ] / gu, '\\$&' ) , 'gu' ) ,
118
+ targetOperator ,
119
+ )
120
+
121
+ return `${ leftText } ${ formattedOperator } ${ rightText } `
122
+ }
123
+
124
+ /**
125
+ * Recursively flattens a logical expression tree into a list of operands and
126
+ * transforms them.
127
+ * @param {FlattenOperandsOptions } options - The flattening options.
128
+ * @returns {string[] } Array of transformed operands.
129
+ */
130
+ let flattenOperands = ( {
131
+ expressionType,
132
+ expression,
133
+ depth = 0 ,
134
+ context,
135
+ } : FlattenOperandsOptions ) : string [ ] => {
136
+ if ( depth > MAX_DEPTH ) {
137
+ return [ toggleNegation ( expression , context ) ]
138
+ }
139
+
140
+ if ( matchesExpressionType ( expression , expressionType ) ) {
141
+ let logicalExpr = expression as LogicalExpression
142
+ return [
143
+ ...flattenOperands ( {
144
+ expression : logicalExpr . left ,
145
+ depth : depth + 1 ,
146
+ expressionType,
147
+ context,
148
+ } ) ,
149
+ ...flattenOperands ( {
150
+ expression : logicalExpr . right ,
151
+ depth : depth + 1 ,
152
+ expressionType,
153
+ context,
154
+ } ) ,
155
+ ]
44
156
}
45
- return isDisjunction ( expr )
157
+
158
+ return [ toggleNegation ( expression , context ) ]
159
+ }
160
+
161
+ /**
162
+ * Transforms a simple logical expression without special formatting.
163
+ * @param {TransformOptions } options - The transformation options.
164
+ * @returns {string } The transformed expression.
165
+ */
166
+ let transformSimple = ( {
167
+ expressionType,
168
+ targetOperator,
169
+ expression,
170
+ context,
171
+ } : TransformSimpleOptions ) : string => {
172
+ let operands = flattenOperands ( {
173
+ expressionType,
174
+ expression,
175
+ context,
176
+ } )
177
+ return operands . join ( ` ${ targetOperator } ` )
46
178
}
47
179
48
180
/**
49
- * Transforms a negated logical expression with preserved formatting according
50
- * to De Morgan's law. Can handle both conjunctions (!(A && B) -> !A || !B) and
51
- * disjunctions (!(A || B) -> !A && !B). Uses the toggleNegation function to
52
- * handle the negation logic consistently .
181
+ * Transforms a negated logical expression according to De Morgan's law.
182
+ * Can handle both conjunctions ` (!(A && B) -> !A || !B)` and disjunctions
183
+ * ` (!(A || B) -> !A && !B)`. Preserves formatting, comments, and whitespace in
184
+ * the transformed expression .
53
185
* @param {TransformOptions } options - The transformation options.
54
186
* @returns {string | null } The transformed expression or null if transformation
55
187
* is not applicable.
@@ -64,60 +196,27 @@ export let transform = ({
64
196
65
197
let sourceOperator : LogicalOperator =
66
198
expressionType === 'conjunction' ? '&&' : '||'
67
- let targetOperator : LogicalOperator = OPERATOR_MAPPING [ sourceOperator ] !
199
+ let targetOperator = OPERATOR_MAPPING [ sourceOperator ] !
68
200
69
201
if ( argument . operator !== sourceOperator ) {
70
202
return null
71
203
}
72
204
73
- let sourceCode = getSourceCode ( context )
74
-
75
- let originalText = sourceCode . getText ( argument )
76
- let hasSpecialFormatting =
77
- originalText . includes ( '//' ) ||
78
- originalText . includes ( '/*' ) ||
79
- originalText . includes ( '\n' ) ||
80
- / \s { 2 , } / u. test ( originalText )
81
-
82
- if ( hasSpecialFormatting && argument . left . range && argument . right . range ) {
83
- let leftText = toggleNegation ( argument . left , context )
84
- let rightText = toggleNegation ( argument . right , context )
85
-
86
- let [ , leftEnd ] = argument . left . range
87
- let [ rightStart ] = argument . right . range
88
- let textBetween = sourceCode . text . slice ( leftEnd , rightStart )
89
-
90
- let formattedOperator = textBetween . replaceAll (
91
- new RegExp (
92
- sourceOperator . replaceAll ( / [ $ ( ) * + . ? [ \\ \] ^ { | } ] / gu, '\\$&' ) ,
93
- 'gu' ,
94
- ) ,
95
- targetOperator ,
96
- )
97
-
98
- let result = `${ leftText } ${ formattedOperator } ${ rightText } `
99
-
100
- return shouldWrapInParens ? `(${ result } )` : result
101
- }
102
-
103
- let flattenOperands = ( expr : Expression , depth = 0 ) : string [ ] => {
104
- if ( depth > MAX_DEPTH ) {
105
- return [ toggleNegation ( expr , context ) ]
106
- }
107
-
108
- if ( matchesExpressionType ( expr , expressionType ) ) {
109
- let logicalExpr = expr as LogicalExpression
110
- return [
111
- ...flattenOperands ( logicalExpr . left , depth + 1 ) ,
112
- ...flattenOperands ( logicalExpr . right , depth + 1 ) ,
113
- ]
114
- }
115
-
116
- return [ toggleNegation ( expr , context ) ]
117
- }
118
-
119
- let operands = flattenOperands ( argument )
120
- let result = operands . join ( ` ${ targetOperator } ` )
205
+ let originalText = getNodeContent ( argument , context )
206
+
207
+ let result = hasSpecialFormatting ( originalText )
208
+ ? transformWithFormatting ( {
209
+ expression : argument ,
210
+ sourceOperator,
211
+ targetOperator,
212
+ context,
213
+ } )
214
+ : transformSimple ( {
215
+ expression : argument ,
216
+ expressionType,
217
+ targetOperator,
218
+ context,
219
+ } )
121
220
122
221
return shouldWrapInParens ? `(${ result } )` : result
123
222
}
0 commit comments