diff --git a/packages/ast-utils/package.json b/packages/ast-utils/package.json index 1fc41b8a..799f361e 100644 --- a/packages/ast-utils/package.json +++ b/packages/ast-utils/package.json @@ -16,6 +16,7 @@ "./identifier": "./src/identifier.ts", "./imports": "./src/imports.ts", "./insert": "./src/insert.ts", + "./kinds": "./src/kinds.ts", "./matchers": "./src/matchers/index.ts", "./object": "./src/object.ts", "./parenthesized": "./src/parenthesized.ts", diff --git a/packages/ast-utils/src/kinds.ts b/packages/ast-utils/src/kinds.ts new file mode 100644 index 00000000..70c704e7 --- /dev/null +++ b/packages/ast-utils/src/kinds.ts @@ -0,0 +1,19 @@ +import { j } from '@wakaru/shared/jscodeshift' + +export const patternKindTypes = [ + j.Identifier, + j.RestElement, + j.SpreadElementPattern, + j.PropertyPattern, + j.ObjectPattern, + j.ArrayPattern, + j.AssignmentPattern, + j.SpreadPropertyPattern, + j.PrivateName, + j.JSXIdentifier, +] +export const memberExpressionKindTypes = [ + j.MemberExpression, + j.OptionalMemberExpression, + j.JSXMemberExpression, +] diff --git a/packages/shared/src/jscodeshift.ts b/packages/shared/src/jscodeshift.ts index 56ba3645..2733a2d2 100644 --- a/packages/shared/src/jscodeshift.ts +++ b/packages/shared/src/jscodeshift.ts @@ -4,11 +4,12 @@ import type { API, Collection } from 'jscodeshift' export const jscodeshiftWithParser = jscodeshift.withParser(babylon()) +export const j = jscodeshiftWithParser + export const toSource = (root: Collection) => { return root.toSource({ lineTerminator: '\n' }) } -const j = jscodeshiftWithParser export const api: API = { j, jscodeshift: j, diff --git a/packages/unminify/src/transformations/__tests__/un-optional-chaining.spec.ts b/packages/unminify/src/transformations/__tests__/un-optional-chaining.spec.ts index d78b0a25..67f12b95 100644 --- a/packages/unminify/src/transformations/__tests__/un-optional-chaining.spec.ts +++ b/packages/unminify/src/transformations/__tests__/un-optional-chaining.spec.ts @@ -790,3 +790,20 @@ null !== (o = null === (s = c.foo.bar) || void 0 === s ? void 0 : s.baz.z) && vo null !== (o = c.foo.bar?.baz.z) && void 0 !== o && o; `, ) + +inlineTest('should not mutate the original AST', + ` +function foo() { + Y || (Y = setTimeout(() => { + (Y = null); + })) +} +`, + ` +function foo() { + Y || (Y = setTimeout(() => { + (Y = null); + })) +} +`, +) diff --git a/packages/unminify/src/transformations/un-optional-chaining.ts b/packages/unminify/src/transformations/un-optional-chaining.ts index a8dff82a..6474dccf 100644 --- a/packages/unminify/src/transformations/un-optional-chaining.ts +++ b/packages/unminify/src/transformations/un-optional-chaining.ts @@ -1,4 +1,5 @@ import { mergeComments } from '@wakaru/ast-utils/comments' +import { memberExpressionKindTypes, patternKindTypes } from '@wakaru/ast-utils/kinds' import { areNodesEqual, isNotNullBinary, isNull, isNullBinary, isTrue, isUndefined, isUndefinedBinary } from '@wakaru/ast-utils/matchers' import { smartParenthesized } from '@wakaru/ast-utils/parenthesized' import { removeDeclarationIfUnused } from '@wakaru/ast-utils/scope' @@ -196,9 +197,7 @@ function applyOptionalChaining( * The output will be a little bit ugly, but it * will eventually be cleaned up by prettier. */ - const object = targetExpression - ? smartParenthesized(j, targetExpression) - : node.object + const object = targetExpression ? smartParenthesized(j, targetExpression) : node.object transformed = true return j.optionalMemberExpression(object, node.property, node.computed) as T } @@ -208,9 +207,7 @@ function applyOptionalChaining( if ((j.CallExpression.check(node) || j.OptionalCallExpression.check(node))) { if ((j.MemberExpression.check(node.callee) || j.OptionalMemberExpression.check(node.callee))) { - if (j.MemberExpression.check(node.callee.object) - && j.Identifier.check(node.callee.property) - ) { + if (j.MemberExpression.check(node.callee.object) && j.Identifier.check(node.callee.property)) { if ( node.callee.property.name === 'call' && areNodesEqual(j, node.arguments[0], tempVariable) @@ -218,12 +215,12 @@ function applyOptionalChaining( const argumentStartsWithThis = areNodesEqual(j, node.arguments[0], tempVariable) const [_, ..._args] = node.arguments const args = argumentStartsWithThis ? _args : node.arguments - const callee = node.callee - const optionalCallExpression = j.optionalCallExpression(callee.object as Identifier, args) - optionalCallExpression.callee = applyOptionalChaining(j, optionalCallExpression.callee, tempVariable, targetExpression) - optionalCallExpression.arguments = optionalCallExpression.arguments.map((arg) => { - return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) - }) + const optionalCallExpression = j.optionalCallExpression( + applyOptionalChaining(j, node.callee.object, tempVariable, targetExpression), + args.map((arg) => { + return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) + }), + ) transformed = true return optionalCallExpression as T } @@ -235,12 +232,12 @@ function applyOptionalChaining( const args = j.ArrayExpression.check(arg) ? arg.elements.map(element => element ?? j.identifier('undefined')) as Array : [j.spreadElement(arg)] - const callee = node.callee - const optionalCallExpression = j.optionalCallExpression(callee.object as Identifier, args) - optionalCallExpression.callee = applyOptionalChaining(j, optionalCallExpression.callee, tempVariable, targetExpression) - optionalCallExpression.arguments = optionalCallExpression.arguments.map((arg) => { - return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) - }) + const optionalCallExpression = j.optionalCallExpression( + applyOptionalChaining(j, node.callee.object, tempVariable, targetExpression), + args.map((arg) => { + return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) + }), + ) transformed = true return optionalCallExpression as T } @@ -251,12 +248,13 @@ function applyOptionalChaining( ) { const calleeObj = node.callee.object const isOptional = !j.AssignmentExpression.check(calleeObj.object) - const memberExpression = isOptional - ? j.optionalMemberExpression(calleeObj.object, calleeObj.property, calleeObj.computed) - : j.memberExpression(calleeObj.object, calleeObj.property, calleeObj.computed) + const builder = isOptional ? j.optionalMemberExpression : j.memberExpression + const memberExpression = builder( + applyOptionalChaining(j, calleeObj.object, tempVariable, targetExpression), + applyOptionalChaining(j, calleeObj.property, tempVariable, targetExpression), + calleeObj.computed, + ) if (isOptional) transformed = true - memberExpression.object = applyOptionalChaining(j, memberExpression.object, tempVariable, targetExpression) - memberExpression.property = applyOptionalChaining(j, memberExpression.property, tempVariable, targetExpression) return memberExpression as T } } @@ -264,11 +262,12 @@ function applyOptionalChaining( if (areNodesEqual(j, node.callee.object, tempVariable)) { if (j.Identifier.check(node.callee.property)) { if (node.callee.property.name === 'call') { - const optionalCallExpression = j.optionalCallExpression(targetExpression as Identifier, node.arguments) - optionalCallExpression.callee = applyOptionalChaining(j, optionalCallExpression.callee, tempVariable, targetExpression) - optionalCallExpression.arguments = optionalCallExpression.arguments.map((arg) => { - return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) - }).splice(1) + const optionalCallExpression = j.optionalCallExpression( + targetExpression as Identifier, + node.arguments.slice(1).map((arg) => { + return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) + }), + ) transformed = true return optionalCallExpression as T } @@ -279,11 +278,12 @@ function applyOptionalChaining( const args = j.ArrayExpression.check(arg) ? arg.elements.map(element => element ?? j.identifier('undefined')) as Array : [j.spreadElement(arg)] - const optionalCallExpression = j.optionalCallExpression(targetExpression as Identifier, args) - optionalCallExpression.callee = applyOptionalChaining(j, optionalCallExpression.callee, tempVariable, targetExpression) - optionalCallExpression.arguments = optionalCallExpression.arguments.map((arg) => { - return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) - }) + const optionalCallExpression = j.optionalCallExpression( + targetExpression as Identifier, + args.map((arg) => { + return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) + }), + ) transformed = true return optionalCallExpression as T } @@ -303,10 +303,10 @@ function applyOptionalChaining( })) { const target = targetExpression || (node.callee as SequenceExpression).expressions[1] const callee = smartParenthesized(j, j.sequenceExpression([j.numericLiteral(0), target])) - const optionalCallExpression = j.optionalCallExpression(callee, node.arguments) - optionalCallExpression.arguments = optionalCallExpression.arguments.map((arg) => { + const args = node.arguments.map((arg) => { return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) }) + const optionalCallExpression = j.optionalCallExpression(callee, args) transformed = true return optionalCallExpression as T } @@ -317,27 +317,34 @@ function applyOptionalChaining( return j.optionalCallExpression(target, node.arguments) as T } - node.callee = applyOptionalChaining(j, node.callee, tempVariable, targetExpression) - node.arguments = node.arguments.map((arg) => { + const isOptional = j.OptionalCallExpression.check(node) + const builder = isOptional ? j.optionalCallExpression : j.callExpression + const callee = applyOptionalChaining(j, node.callee, tempVariable, targetExpression) + const args = node.arguments.map((arg) => { return j.SpreadElement.check(arg) ? arg : applyOptionalChaining(j, arg, tempVariable, targetExpression) }) + return builder(callee, args) as T } if (j.AssignmentExpression.check(node)) { - if (areNodesEqual(j, node.left, tempVariable) && targetExpression) { + if (targetExpression && areNodesEqual(j, node.left, tempVariable)) { if (node.right === targetExpression) { return targetExpression as T } - node.left = targetExpression as any + + if (memberExpressionKindTypes.some(type => type.check(targetExpression)) || patternKindTypes.some(type => type.check(targetExpression))) { + node = j.assignmentExpression(node.operator, targetExpression as any, node.right) as T + } } } - if (j.Identifier.check(node) && areNodesEqual(j, node, tempVariable) && targetExpression) { + if (targetExpression && j.Identifier.check(node) && areNodesEqual(j, node, tempVariable)) { return smartParenthesized(j, targetExpression) as T } if (j.UnaryExpression.check(node)) { - node.argument = applyOptionalChaining(j, node.argument, tempVariable, targetExpression) + const arg = applyOptionalChaining(j, node.argument, tempVariable, targetExpression) + node = j.unaryExpression(node.operator, arg, node.prefix) as T } return node