UNPKG

eslint-plugin-de-morgan

Version:

ESLint plugin for transforming negated boolean expressions via De Morgan’s laws

190 lines (189 loc) 5.63 kB
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 }