diff --git a/apps/docs/src/concepts/unminify.md b/apps/docs/src/concepts/unminify.md index 8ad8fd63..2c03ef09 100644 --- a/apps/docs/src/concepts/unminify.md +++ b/apps/docs/src/concepts/unminify.md @@ -27,6 +27,23 @@ console.log(a); // [!code ++] Infinity // [!code ++] ``` +## invert-boolean-logic + +```js +!(a == b) // [!code --] +a != b // [!code ++] +``` + +```js +!(a || b || c) // [!code --] +!a && !b && !c // [!code ++] +``` + +```js +!(a && b && c) // [!code --] +!a || !b || !c // [!code ++] +``` + ## json-parse ```js diff --git a/packages/webcrack/src/unminify/test/invert-boolean-logic.test.ts b/packages/webcrack/src/unminify/test/invert-boolean-logic.test.ts new file mode 100644 index 00000000..2ae69197 --- /dev/null +++ b/packages/webcrack/src/unminify/test/invert-boolean-logic.test.ts @@ -0,0 +1,34 @@ +import { test } from 'vitest'; +import { testTransform } from '../../../test'; +import invertBooleanLogic from '../transforms/invert-boolean-logic'; + +const expectJS = testTransform(invertBooleanLogic); + +test('loose equal', () => + expectJS('!(a == b);').toMatchInlineSnapshot(`a != b;`)); + +test('strict equal', () => + expectJS('!(a === b);').toMatchInlineSnapshot(`a !== b;`)); + +test('not equal', () => + expectJS('!(a != b);').toMatchInlineSnapshot(`a == b;`)); + +test('not strict equal', () => + expectJS('!(a !== b);').toMatchInlineSnapshot(`a === b;`)); + +test('greater than', () => + expectJS('!(a > b);').toMatchInlineSnapshot(`a <= b;`)); + +test('less than', () => expectJS('!(a < b);').toMatchInlineSnapshot(`a >= b;`)); + +test('greater than or equal', () => + expectJS('!(a >= b);').toMatchInlineSnapshot(`a < b;`)); + +test('less than or equal', () => + expectJS('!(a <= b);').toMatchInlineSnapshot(`a > b;`)); + +test('logical or', () => + expectJS('!(a || b || c);').toMatchInlineSnapshot(`!a && !b && !c;`)); + +test('logical and', () => + expectJS('!(a && b && c);').toMatchInlineSnapshot(`!a || !b || !c;`)); diff --git a/packages/webcrack/src/unminify/transforms/index.ts b/packages/webcrack/src/unminify/transforms/index.ts index feb894d7..63eb1552 100644 --- a/packages/webcrack/src/unminify/transforms/index.ts +++ b/packages/webcrack/src/unminify/transforms/index.ts @@ -1,6 +1,7 @@ export { default as blockStatements } from './block-statements'; export { default as computedProperties } from './computed-properties'; export { default as infinity } from './infinity'; +export { default as invertBooleanLogic } from './invert-boolean-logic'; export { default as jsonParse } from './json-parse'; export { default as logicalToIf } from './logical-to-if'; export { default as mergeElseIf } from './merge-else-if'; diff --git a/packages/webcrack/src/unminify/transforms/invert-boolean-logic.ts b/packages/webcrack/src/unminify/transforms/invert-boolean-logic.ts new file mode 100644 index 00000000..f78ebc60 --- /dev/null +++ b/packages/webcrack/src/unminify/transforms/invert-boolean-logic.ts @@ -0,0 +1,71 @@ +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { Transform } from '../../ast-utils'; + +const INVERTED_BINARY_OPERATORS = { + '==': '!=', + '===': '!==', + '!=': '==', + '!==': '===', + '>': '<=', + '<': '>=', + '>=': '<', + '<=': '>', +} as const; + +const INVERTED_LOGICAL_OPERATORS = { + '||': '&&', + '&&': '||', +} as const; + +export default { + name: 'invert-boolean-logic', + tags: ['safe'], + visitor: () => { + const logicalExpression = m.logicalExpression( + m.or(...Object.values(INVERTED_LOGICAL_OPERATORS)), + ); + const logicalMatcher = m.unaryExpression('!', logicalExpression); + + const binaryExpression = m.capture( + m.binaryExpression(m.or(...Object.values(INVERTED_BINARY_OPERATORS))), + ); + const binaryMatcher = m.unaryExpression('!', binaryExpression); + + return { + UnaryExpression: { + exit(path) { + const { argument } = path.node; + + if (binaryMatcher.match(path.node)) { + binaryExpression.current!.operator = + INVERTED_BINARY_OPERATORS[ + binaryExpression.current! + .operator as keyof typeof INVERTED_BINARY_OPERATORS + ]; + + path.replaceWith(binaryExpression.current!); + this.changes++; + } else if (logicalMatcher.match(path.node)) { + let current = argument; + while (logicalExpression.match(current)) { + current.operator = + INVERTED_LOGICAL_OPERATORS[ + current.operator as keyof typeof INVERTED_LOGICAL_OPERATORS + ]; + + current.right = t.unaryExpression('!', current.right); + if (current.left.type !== 'LogicalExpression') { + current.left = t.unaryExpression('!', current.left); + } + current = current.left; + } + + path.replaceWith(argument); + this.changes++; + } + }, + }, + }; + }, +} satisfies Transform;