Skip to content

Commit

Permalink
feat(un-assignment-merging): add new rule un-assignment-merging for…
Browse files Browse the repository at this point in the history
… spliting chained assignment
  • Loading branch information
pionxzh committed Nov 26, 2023
1 parent 4e444c5 commit 59e2929
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 1 deletion.
12 changes: 12 additions & 0 deletions packages/unminify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ It covers most of patterns that are used by the following tools:
- [`un-typeof`](#un-typeof)
- [`un-sequence-expression`](#un-sequence-expression)
- [`un-variable-merging`](#un-variable-merging)
- [`un-assignment-expression`](#un-assignment-expression)
- [`un-bracket-notation`](#un-bracket-notation)
- [`un-while-loop`](#un-while-loop)
- [`un-flip-comparisons`](#un-flip-comparisons)
Expand Down Expand Up @@ -148,6 +149,17 @@ Separate variable declarators that are not used in for statements.
+ for (var j = 0, k = 0; j < 10; k++) {}
```

### `un-assignment-expression`

Separate chained assignment into multiple statements.

```diff
- a = b = c = 1
+ a = 1
+ b = 1
+ c = 1
```

### `un-bracket-notation`

Simplify bracket notation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { defineInlineTest } from '@wakaru/test-utils'
import transform from '../un-assignment-merging'

const inlineTest = defineInlineTest(transform)

inlineTest('chained assignment should be splitted',
`
exports.foo = exports.bar = exports.baz = 1;
`,
`
exports.foo = 1;
exports.bar = 1;
exports.baz = 1;
`,
)

inlineTest('chained assignment should be splitted - allowed',
`
a1 = a2 = 0;
b1 = b2 = 0n;
c1 = c2 = '';
d1 = d2 = true;
e1 = e2 = null;
f1 = f2 = undefined;
g1 = g2 = foo;
`,
`
a1 = 0;
a2 = 0;
b1 = 0n;
b2 = 0n;
c1 = '';
c2 = '';
d1 = true;
d2 = true;
e1 = null;
e2 = null;
f1 = undefined;
f2 = undefined;
g1 = foo;
g2 = foo;
`,
)

inlineTest('chained assignment should be splitted - not allowed',
`
a1 = a2 = \`template\${foo}\`;
b1 = b2 = Symbol();
c1 = c2 = /regex/;
d1 = d2 = foo.bar;
f1 = f2 = fn();
`,
`
a1 = a2 = \`template\${foo}\`;
b1 = b2 = Symbol();
c1 = c2 = /regex/;
d1 = d2 = foo.bar;
f1 = f2 = fn();
`,
)

inlineTest('chained assignment should be splitted - comments',
`
// before
exports.foo = exports.bar = exports.baz = 1;
// after
`,
`
// before
exports.foo = 1;
exports.bar = 1;
exports.baz = 1;
// after
`,
)
4 changes: 3 additions & 1 deletion packages/unminify/src/transformations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import moduleMapping from './module-mapping'
import prettier from './prettier'
import smartInline from './smart-inline'
import smartRename from './smart-rename'
import unAssignmentMerging from './un-assignment-merging'
import unAsyncAwait from './un-async-await'
import unBoolean from './un-boolean'
import unBracketNotation from './un-bracket-notation'
Expand Down Expand Up @@ -44,10 +45,11 @@ export const transformationMap: {
'un-curly-braces': unCurlyBraces, // add curly braces so that other transformations can works easier, but generally this is not required
'un-sequence-expression1': unSequenceExpression, // curly braces can bring out return sequence expression, so it runs before this
'un-variable-merging': unVariableMerging,
'un-assignment-merging': unAssignmentMerging,

// second stage - prepare the code for unminify
'un-runtime-helper': unRuntimeHelper, // eliminate helpers as early as possible
'un-esm': unEsm, // relies on `un-runtime-helper` to eliminate helpers around `require` calls
'un-esm': unEsm, // relies on `un-runtime-helper` to eliminate helpers around `require` calls, relies on `un-assignment-merging` to split exports
'un-enum': unEnum, // relies on `un-sequence-expression`

// third stage - mostly one-to-one transformation
Expand Down
82 changes: 82 additions & 0 deletions packages/unminify/src/transformations/un-assignment-merging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { mergeComments } from '@wakaru/ast-utils'
import { isSimpleValue } from '../utils/checker'
import { replaceWithMultipleStatements } from '../utils/insert'
import wrap from '../wrapAstTransformation'
import type { ASTTransformation } from '../wrapAstTransformation'
import type { AssignmentExpression } from 'jscodeshift'

/**
* Separate chained assignment into multiple statements.
* This rule is only applied to simple value assignment to
* avoid introducing behavior changes.
*
* Normally, this rule should assign the next variable to the
* previous one, which is also how the code is executed.
*
* For example:
* ```js
* exports.foo = exports.bar = 1
* -> should be
* exports.bar = 1
* exports.foo = exports.bar
* ```
*
* But instead, in this rule, it is assigned to the original value
* to maximize the readability, and ease some edge cases that other
* rules might hit on.
*
* @example
* exports.foo = exports.bar = 1
* ->
* exports.bar = 1
* exports.foo = 1
*
* @example
* foo = bar = baz = void 0
* ->
* foo = void 0
* bar = void 0
* baz = void 0
*/
export const transformAST: ASTTransformation = (context) => {
const { root, j } = context

root
.find(j.ExpressionStatement, {
expression: {
type: 'AssignmentExpression',
operator: '=',
right: {
type: 'AssignmentExpression',
operator: '=',
},
},
})
.forEach((p) => {
const { expression } = p.node

let node = expression as AssignmentExpression
const assignments: AssignmentExpression[] = [node]
while (j.AssignmentExpression.check(node.right)) {
node = node.right
assignments.push(node)
}

if (assignments.length < 2) return

const valueNode = node.right
if (
j.Identifier.check(valueNode)
|| isSimpleValue(j, valueNode)
) {
const replacements = assignments.map((assignment) => {
return j.expressionStatement(j.assignmentExpression('=', assignment.left, valueNode))
})
mergeComments(replacements, p.node.comments)

replaceWithMultipleStatements(j, p, replacements)
}
})
}

export default wrap(transformAST)
40 changes: 40 additions & 0 deletions packages/unminify/src/utils/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,46 @@ export function areNodesEqual(j: JSCodeshift, node1: ASTNode, node2: ASTNode): b
return j(node1).toSource() === j(node2).toSource()
}

/**
* Check if node is a simple value.
* Meaning it's duplication won't cause any side effects.
*
* Includes:
* - string
* - number
* - bigInt
* - boolean
* - undefined
* - null
*
* Excludes:
* - template literal (might contain other expressions)
* - symbol ( Symbol() !== Symbol() )
* - MemberExpression (might have side effects)
* - CallExpression (might have side effects)
*/
export function isSimpleValue(j: JSCodeshift, node: ASTNode): node is StringLiteral | NumericLiteral | BooleanLiteral | NullLiteral | UnaryExpression | Identifier | BigIntLiteral {
return (
j.NullLiteral.check(node)
|| j.StringLiteral.check(node)
|| j.NumericLiteral.check(node)
|| isUndefined(j, node)
|| isLooseBoolean(j, node)
|| j.BigIntLiteral.check(node)
)
}

/**
* Check if node is a loose boolean-like value.
* Includes:
* - Boolean
* - !0, !1 and all ![thing]
*/
export function isLooseBoolean(j: JSCodeshift, node: ASTNode): node is BooleanLiteral | UnaryExpression {
return j.BooleanLiteral.check(node)
|| (j.UnaryExpression.check(node) && node.operator === '!')
}

/**
* Check if node is `true` literal
*/
Expand Down

0 comments on commit 59e2929

Please sign in to comment.