eslint-plugin-de-morgan
Version:
ESLint plugin for transforming negated boolean expressions via De Morgan’s laws
190 lines (189 loc) • 5.63 kB
JavaScript
import { isConjunction } from './is-conjunction.js'
import { parenthesize } from './parenthesize.js'
import { toggleNegation } from './toggle-negation.js'
import { isDisjunction } from './is-disjunction.js'
var MAX_DEPTH = 10
var OPERATOR_MAPPING = {
'&&': '||',
'||': '&&',
}
/**
* Transforms a negated logical expression according to De Morgan's law. Can
* handle both conjunctions `(!(A && B) -> !A || !B)` and disjunctions `(!(A ||
* B) -> !A && !B)`. Preserves formatting, comments, and whitespace in the
* transformed expression.
*
* @param options - The transformation options.
* @returns The transformed expression or null if transformation is not
* applicable.
*/
function transform({
shouldWrapInParens,
canStripNegation,
expressionType,
context,
node,
}) {
let argument = node.argument
let sourceOperator = expressionType === 'conjunction' ? '&&' : '||'
if (argument.operator !== sourceOperator) {
return null
}
let originalText = context.sourceCode.getText(argument)
let transformUtilityOptions = {
expression: argument,
canStripNegation,
expressionType,
sourceOperator,
targetOperator: OPERATOR_MAPPING[sourceOperator],
context,
}
return parenthesize(
hasSpecialFormatting(originalText) ?
transformWithFormatting(transformUtilityOptions)
: transformSimple(transformUtilityOptions),
shouldWrapInParens,
)
}
/**
* Transforms an expression with special formatting (comments, multiple spaces).
*
* @param options - The transformation options.
* @returns The transformed expression with preserved formatting.
*/
function transformWithFormatting({
canStripNegation,
sourceOperator,
targetOperator,
expression,
context,
}) {
let { sourceCode } = context
let leftText = toggleNegation(expression.left, context, canStripNegation)
let rightText = toggleNegation(expression.right, context, canStripNegation)
if (!expression.left.range || !expression.right.range) {
return `${leftText} ${targetOperator} ${rightText}`
}
let [, leftEnd] = expression.left.range
let [rightStart] = expression.right.range
return `${leftText}${normalizeTextBetweenOperands(sourceCode.text.slice(leftEnd, rightStart)).replaceAll(new RegExp(sourceOperator.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`), 'gu'), targetOperator)}${rightText}`
}
/**
* Iteratively flattens a logical expression tree into a list of operands and
* transforms them using a stack-based approach for better performance.
*
* @param options - The flattening options.
* @returns Array of transformed operands.
*/
function flattenOperands({
canStripNegation,
expressionType,
expression,
context,
}) {
let result = []
let stack = [
{
expr: expression,
depth: 0,
},
]
while (stack.length > 0) {
let { depth, expr } = stack.pop()
if (depth > MAX_DEPTH || !matchesExpressionType(expr, expressionType)) {
result.push(toggleNegation(expr, context, canStripNegation))
continue
}
let logicalExpression = expr
stack.push(
{
expr: logicalExpression.right,
depth: depth + 1,
},
{
expr: logicalExpression.left,
depth: depth + 1,
},
)
}
return result
}
/**
* Transforms a simple logical expression without special formatting.
*
* @param options - The transformation options.
* @returns The transformed expression.
*/
function transformSimple({
canStripNegation,
expressionType,
targetOperator,
expression,
context,
}) {
return flattenOperands({
canStripNegation,
expressionType,
expression,
context,
}).join(` ${targetOperator} `)
}
/**
* Checks if the expression matches the specified logical type.
*
* @param expression - The expression to check.
* @param type - The type to check against.
* @returns True if the expression matches the type, false otherwise.
*/
function matchesExpressionType(expression, type) {
return type === 'conjunction' ?
isConjunction(expression)
: isDisjunction(expression)
}
/**
* Checks if the text contains special formatting like comments or multiple
* spaces.
*
* @param text - The text to check.
* @returns True if the text contains special formatting.
*/
function hasSpecialFormatting(text) {
return (
text.includes('//') ||
text.includes('/*') ||
text.includes('\n') ||
/\s{2,}/u.test(text)
)
}
/**
* Removes boundary grouping parentheses from the text between operands while
* preserving the original spacing, line breaks, and comments around the logical
* operator.
*
* @param textBetween - The original text between the left and right operands.
* @returns Normalized text without boundary grouping parentheses.
*/
function normalizeTextBetweenOperands(textBetween) {
return removeTrailingGroupingParens(removeLeadingGroupingParens(textBetween))
}
/**
* Removes trailing opening grouping parentheses from the text between operands
* while preserving any leading whitespace.
*
* @param text - The text between operands.
* @returns Text without trailing opening grouping parentheses.
*/
function removeTrailingGroupingParens(text) {
return text.replace(/(?:\s*\()+\s*$/u, match => match.match(/^\s*/u)[0])
}
/**
* Removes leading closing grouping parentheses from the text between operands
* while preserving any trailing whitespace.
*
* @param text - The text between operands.
* @returns Text without leading closing grouping parentheses.
*/
function removeLeadingGroupingParens(text) {
return text.replace(/^\s*(?:\)\s*)+/u, match => match.match(/\s*$/u)[0])
}
export { transform }