Skip to content

Commit 7517e26

Browse files
committed
refactor: reduce transform func complexity
1 parent e78df10 commit 7517e26

File tree

2 files changed

+201
-62
lines changed

2 files changed

+201
-62
lines changed

test/utils/transform.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -331,4 +331,44 @@ describe('transform', () => {
331331
let operatorCount = (result?.match(/\|\|/gu) ?? []).length
332332
expect(operatorCount).toBeLessThan(15)
333333
})
334+
335+
it('should handle special formatting with missing ranges', () => {
336+
let leftId = {
337+
type: 'Identifier',
338+
range: [0, 1],
339+
id: 'id_a',
340+
name: 'a',
341+
raw: 'a',
342+
}
343+
344+
let rightId = {
345+
type: 'Identifier',
346+
id: 'id_b',
347+
name: 'b',
348+
raw: 'b',
349+
}
350+
351+
let conjunction = {
352+
type: 'LogicalExpression',
353+
id: 'conjunction',
354+
raw: 'a && b',
355+
operator: '&&',
356+
right: rightId,
357+
range: [0, 8],
358+
left: leftId,
359+
}
360+
361+
let unaryExpr = createUnaryExpression(conjunction as FakeLogicalExpression)
362+
363+
let context = createFakeContext('a && b')
364+
365+
let result = transform({
366+
node: unaryExpr as unknown as UnaryExpression,
367+
expressionType: 'conjunction',
368+
shouldWrapInParens: false,
369+
context,
370+
})
371+
372+
expect(result).toBe('!a || !b')
373+
})
334374
})

utils/transform.ts

+161-62
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import type {
66
} from 'estree'
77
import type { Rule } from 'eslint'
88

9+
import { getNodeContent } from './get-node-content'
910
import { toggleNegation } from './toggle-negation'
1011
import { getSourceCode } from './get-source-code'
1112
import { isConjunction } from './is-conjunction'
1213
import { isDisjunction } from './is-disjunction'
1314

1415
interface TransformOptions {
1516
/** The type of logical expression to transform. */
16-
expressionType: 'conjunction' | 'disjunction'
17+
expressionType: ExpressionType
1718
/** Whether the transformed expression should be wrapped in parentheses. */
1819
shouldWrapInParens: boolean
1920
/** The ESLint rule context. */
@@ -22,6 +23,41 @@ interface TransformOptions {
2223
node: UnaryExpression
2324
}
2425

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+
2561
const MAX_DEPTH = 10
2662

2763
const OPERATOR_MAPPING: Partial<Record<LogicalOperator, LogicalOperator>> = {
@@ -31,25 +67,121 @@ const OPERATOR_MAPPING: Partial<Record<LogicalOperator, LogicalOperator>> = {
3167

3268
/**
3369
* 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.
3672
* @returns {boolean} True if the expression matches the type, false otherwise.
3773
*/
3874
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+
]
44156
}
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} `)
46178
}
47179

48180
/**
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.
53185
* @param {TransformOptions} options - The transformation options.
54186
* @returns {string | null} The transformed expression or null if transformation
55187
* is not applicable.
@@ -64,60 +196,27 @@ export let transform = ({
64196

65197
let sourceOperator: LogicalOperator =
66198
expressionType === 'conjunction' ? '&&' : '||'
67-
let targetOperator: LogicalOperator = OPERATOR_MAPPING[sourceOperator]!
199+
let targetOperator = OPERATOR_MAPPING[sourceOperator]!
68200

69201
if (argument.operator !== sourceOperator) {
70202
return null
71203
}
72204

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+
})
121220

122221
return shouldWrapInParens ? `(${result})` : result
123222
}

0 commit comments

Comments
 (0)