webfuscator 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +9 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/analysis/constant.d.ts +11 -0
- package/dist/analysis/constant.d.ts.map +1 -0
- package/dist/analysis/constant.js +261 -0
- package/dist/analysis/constant.js.map +1 -0
- package/dist/analysis/document-order.d.ts +5 -0
- package/dist/analysis/document-order.d.ts.map +1 -0
- package/dist/analysis/document-order.js +124 -0
- package/dist/analysis/document-order.js.map +1 -0
- package/dist/analysis/inlineability.d.ts +13 -0
- package/dist/analysis/inlineability.d.ts.map +1 -0
- package/dist/analysis/inlineability.js +270 -0
- package/dist/analysis/inlineability.js.map +1 -0
- package/dist/analysis/purity.d.ts +5 -0
- package/dist/analysis/purity.d.ts.map +1 -0
- package/dist/analysis/purity.js +281 -0
- package/dist/analysis/purity.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/obfuscator.d.ts +4 -0
- package/dist/obfuscator.d.ts.map +1 -0
- package/dist/obfuscator.js +222 -0
- package/dist/obfuscator.js.map +1 -0
- package/dist/options.d.ts +73 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +2 -0
- package/dist/options.js.map +1 -0
- package/dist/preparation/dot-to-bracket.d.ts +16 -0
- package/dist/preparation/dot-to-bracket.d.ts.map +1 -0
- package/dist/preparation/dot-to-bracket.js +36 -0
- package/dist/preparation/dot-to-bracket.js.map +1 -0
- package/dist/preparation/expand-destructuring.d.ts +26 -0
- package/dist/preparation/expand-destructuring.d.ts.map +1 -0
- package/dist/preparation/expand-destructuring.js +154 -0
- package/dist/preparation/expand-destructuring.js.map +1 -0
- package/dist/preparation/expand-template-literals.d.ts +23 -0
- package/dist/preparation/expand-template-literals.d.ts.map +1 -0
- package/dist/preparation/expand-template-literals.js +78 -0
- package/dist/preparation/expand-template-literals.js.map +1 -0
- package/dist/preparation/rewrite-class-fields.d.ts +43 -0
- package/dist/preparation/rewrite-class-fields.d.ts.map +1 -0
- package/dist/preparation/rewrite-class-fields.js +270 -0
- package/dist/preparation/rewrite-class-fields.js.map +1 -0
- package/dist/preparation/split-sequence-expressions.d.ts +15 -0
- package/dist/preparation/split-sequence-expressions.d.ts.map +1 -0
- package/dist/preparation/split-sequence-expressions.js +36 -0
- package/dist/preparation/split-sequence-expressions.js.map +1 -0
- package/dist/preparation/split-variable-declarations.d.ts +20 -0
- package/dist/preparation/split-variable-declarations.d.ts.map +1 -0
- package/dist/preparation/split-variable-declarations.js +36 -0
- package/dist/preparation/split-variable-declarations.js.map +1 -0
- package/dist/preparation/stringify-object-keys.d.ts +21 -0
- package/dist/preparation/stringify-object-keys.d.ts.map +1 -0
- package/dist/preparation/stringify-object-keys.js +96 -0
- package/dist/preparation/stringify-object-keys.js.map +1 -0
- package/dist/preparation/wrap-single-statements.d.ts +24 -0
- package/dist/preparation/wrap-single-statements.d.ts.map +1 -0
- package/dist/preparation/wrap-single-statements.js +53 -0
- package/dist/preparation/wrap-single-statements.js.map +1 -0
- package/dist/transforms/arguments-to-parameters.d.ts +20 -0
- package/dist/transforms/arguments-to-parameters.d.ts.map +1 -0
- package/dist/transforms/arguments-to-parameters.js +162 -0
- package/dist/transforms/arguments-to-parameters.js.map +1 -0
- package/dist/transforms/arrow-to-function.d.ts +46 -0
- package/dist/transforms/arrow-to-function.d.ts.map +1 -0
- package/dist/transforms/arrow-to-function.js +203 -0
- package/dist/transforms/arrow-to-function.js.map +1 -0
- package/dist/transforms/collapse-single-use-temps.d.ts +16 -0
- package/dist/transforms/collapse-single-use-temps.d.ts.map +1 -0
- package/dist/transforms/collapse-single-use-temps.js +805 -0
- package/dist/transforms/collapse-single-use-temps.js.map +1 -0
- package/dist/transforms/const-let-to-var.d.ts +21 -0
- package/dist/transforms/const-let-to-var.d.ts.map +1 -0
- package/dist/transforms/const-let-to-var.js +217 -0
- package/dist/transforms/const-let-to-var.js.map +1 -0
- package/dist/transforms/const-to-let.d.ts +17 -0
- package/dist/transforms/const-to-let.d.ts.map +1 -0
- package/dist/transforms/const-to-let.js +33 -0
- package/dist/transforms/const-to-let.js.map +1 -0
- package/dist/transforms/do-while-to-while.d.ts +26 -0
- package/dist/transforms/do-while-to-while.d.ts.map +1 -0
- package/dist/transforms/do-while-to-while.js +54 -0
- package/dist/transforms/do-while-to-while.js.map +1 -0
- package/dist/transforms/drop-console.d.ts +27 -0
- package/dist/transforms/drop-console.d.ts.map +1 -0
- package/dist/transforms/drop-console.js +107 -0
- package/dist/transforms/drop-console.js.map +1 -0
- package/dist/transforms/drop-debugger.d.ts +18 -0
- package/dist/transforms/drop-debugger.d.ts.map +1 -0
- package/dist/transforms/drop-debugger.js +34 -0
- package/dist/transforms/drop-debugger.js.map +1 -0
- package/dist/transforms/expand-binary-assignment.d.ts +26 -0
- package/dist/transforms/expand-binary-assignment.d.ts.map +1 -0
- package/dist/transforms/expand-binary-assignment.js +78 -0
- package/dist/transforms/expand-binary-assignment.js.map +1 -0
- package/dist/transforms/expand-logical-assignment.d.ts +22 -0
- package/dist/transforms/expand-logical-assignment.d.ts.map +1 -0
- package/dist/transforms/expand-logical-assignment.js +73 -0
- package/dist/transforms/expand-logical-assignment.js.map +1 -0
- package/dist/transforms/extract-object-properties.d.ts +21 -0
- package/dist/transforms/extract-object-properties.d.ts.map +1 -0
- package/dist/transforms/extract-object-properties.js +340 -0
- package/dist/transforms/extract-object-properties.js.map +1 -0
- package/dist/transforms/fold-builtin-methods.d.ts +23 -0
- package/dist/transforms/fold-builtin-methods.d.ts.map +1 -0
- package/dist/transforms/fold-builtin-methods.js +839 -0
- package/dist/transforms/fold-builtin-methods.js.map +1 -0
- package/dist/transforms/fold-constants.d.ts +16 -0
- package/dist/transforms/fold-constants.d.ts.map +1 -0
- package/dist/transforms/fold-constants.js +421 -0
- package/dist/transforms/fold-constants.js.map +1 -0
- package/dist/transforms/for-to-while.d.ts +29 -0
- package/dist/transforms/for-to-while.d.ts.map +1 -0
- package/dist/transforms/for-to-while.js +91 -0
- package/dist/transforms/for-to-while.js.map +1 -0
- package/dist/transforms/function-declaration-to-expression.d.ts +28 -0
- package/dist/transforms/function-declaration-to-expression.d.ts.map +1 -0
- package/dist/transforms/function-declaration-to-expression.js +95 -0
- package/dist/transforms/function-declaration-to-expression.js.map +1 -0
- package/dist/transforms/inline-functions.d.ts +16 -0
- package/dist/transforms/inline-functions.d.ts.map +1 -0
- package/dist/transforms/inline-functions.js +896 -0
- package/dist/transforms/inline-functions.js.map +1 -0
- package/dist/transforms/logical-to-ternary.d.ts +20 -0
- package/dist/transforms/logical-to-ternary.d.ts.map +1 -0
- package/dist/transforms/logical-to-ternary.js +78 -0
- package/dist/transforms/logical-to-ternary.js.map +1 -0
- package/dist/transforms/mangle-properties.d.ts +27 -0
- package/dist/transforms/mangle-properties.d.ts.map +1 -0
- package/dist/transforms/mangle-properties.js +703 -0
- package/dist/transforms/mangle-properties.js.map +1 -0
- package/dist/transforms/nullish-coalescing-to-ternary.d.ts +18 -0
- package/dist/transforms/nullish-coalescing-to-ternary.d.ts.map +1 -0
- package/dist/transforms/nullish-coalescing-to-ternary.js +61 -0
- package/dist/transforms/nullish-coalescing-to-ternary.js.map +1 -0
- package/dist/transforms/numbers-to-strings.d.ts +19 -0
- package/dist/transforms/numbers-to-strings.d.ts.map +1 -0
- package/dist/transforms/numbers-to-strings.js +51 -0
- package/dist/transforms/numbers-to-strings.js.map +1 -0
- package/dist/transforms/object-method-to-property.d.ts +34 -0
- package/dist/transforms/object-method-to-property.d.ts.map +1 -0
- package/dist/transforms/object-method-to-property.js +78 -0
- package/dist/transforms/object-method-to-property.js.map +1 -0
- package/dist/transforms/optional-chaining-to-ternary.d.ts +19 -0
- package/dist/transforms/optional-chaining-to-ternary.d.ts.map +1 -0
- package/dist/transforms/optional-chaining-to-ternary.js +206 -0
- package/dist/transforms/optional-chaining-to-ternary.js.map +1 -0
- package/dist/transforms/pack-declarations-into-parameters.d.ts +22 -0
- package/dist/transforms/pack-declarations-into-parameters.d.ts.map +1 -0
- package/dist/transforms/pack-declarations-into-parameters.js +208 -0
- package/dist/transforms/pack-declarations-into-parameters.js.map +1 -0
- package/dist/transforms/remove-anonymous-function-names.d.ts +17 -0
- package/dist/transforms/remove-anonymous-function-names.d.ts.map +1 -0
- package/dist/transforms/remove-anonymous-function-names.js +72 -0
- package/dist/transforms/remove-anonymous-function-names.js.map +1 -0
- package/dist/transforms/remove-unreachable-code.d.ts +21 -0
- package/dist/transforms/remove-unreachable-code.d.ts.map +1 -0
- package/dist/transforms/remove-unreachable-code.js +103 -0
- package/dist/transforms/remove-unreachable-code.js.map +1 -0
- package/dist/transforms/remove-unused-code.d.ts +18 -0
- package/dist/transforms/remove-unused-code.d.ts.map +1 -0
- package/dist/transforms/remove-unused-code.js +249 -0
- package/dist/transforms/remove-unused-code.js.map +1 -0
- package/dist/transforms/rename-identifiers.d.ts +24 -0
- package/dist/transforms/rename-identifiers.d.ts.map +1 -0
- package/dist/transforms/rename-identifiers.js +264 -0
- package/dist/transforms/rename-identifiers.js.map +1 -0
- package/dist/transforms/specials-to-strings.d.ts +30 -0
- package/dist/transforms/specials-to-strings.d.ts.map +1 -0
- package/dist/transforms/specials-to-strings.js +101 -0
- package/dist/transforms/specials-to-strings.js.map +1 -0
- package/dist/transforms/switch-to-if.d.ts +28 -0
- package/dist/transforms/switch-to-if.d.ts.map +1 -0
- package/dist/transforms/switch-to-if.js +279 -0
- package/dist/transforms/switch-to-if.js.map +1 -0
- package/dist/transforms/ternary-to-if.d.ts +20 -0
- package/dist/transforms/ternary-to-if.d.ts.map +1 -0
- package/dist/transforms/ternary-to-if.js +315 -0
- package/dist/transforms/ternary-to-if.js.map +1 -0
- package/dist/transforms/update-to-assignment.d.ts +26 -0
- package/dist/transforms/update-to-assignment.d.ts.map +1 -0
- package/dist/transforms/update-to-assignment.js +120 -0
- package/dist/transforms/update-to-assignment.js.map +1 -0
- package/dist/transforms/yodify-conditions.d.ts +22 -0
- package/dist/transforms/yodify-conditions.d.ts.map +1 -0
- package/dist/transforms/yodify-conditions.js +91 -0
- package/dist/transforms/yodify-conditions.js.map +1 -0
- package/dist/utils/ast.d.ts +10 -0
- package/dist/utils/ast.d.ts.map +1 -0
- package/dist/utils/ast.js +112 -0
- package/dist/utils/ast.js.map +1 -0
- package/dist/utils/change-tracking.d.ts +8 -0
- package/dist/utils/change-tracking.d.ts.map +1 -0
- package/dist/utils/change-tracking.js +8 -0
- package/dist/utils/change-tracking.js.map +1 -0
- package/dist/utils/dom-properties.d.ts +2 -0
- package/dist/utils/dom-properties.d.ts.map +1 -0
- package/dist/utils/dom-properties.js +9252 -0
- package/dist/utils/dom-properties.js.map +1 -0
- package/dist/utils/evaluation-order.d.ts +4 -0
- package/dist/utils/evaluation-order.d.ts.map +1 -0
- package/dist/utils/evaluation-order.js +152 -0
- package/dist/utils/evaluation-order.js.map +1 -0
- package/dist/utils/literal.d.ts +5 -0
- package/dist/utils/literal.d.ts.map +1 -0
- package/dist/utils/literal.js +90 -0
- package/dist/utils/literal.js.map +1 -0
- package/dist/utils/logical-lowering.d.ts +4 -0
- package/dist/utils/logical-lowering.d.ts.map +1 -0
- package/dist/utils/logical-lowering.js +104 -0
- package/dist/utils/logical-lowering.js.map +1 -0
- package/dist/utils/loop-lowering.d.ts +23 -0
- package/dist/utils/loop-lowering.d.ts.map +1 -0
- package/dist/utils/loop-lowering.js +88 -0
- package/dist/utils/loop-lowering.js.map +1 -0
- package/dist/utils/paths.d.ts +9 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +90 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/random.d.ts +3 -0
- package/dist/utils/random.d.ts.map +1 -0
- package/dist/utils/random.js +22 -0
- package/dist/utils/random.js.map +1 -0
- package/dist/utils/string-generator.d.ts +37 -0
- package/dist/utils/string-generator.d.ts.map +1 -0
- package/dist/utils/string-generator.js +175 -0
- package/dist/utils/string-generator.js.map +1 -0
- package/package.json +76 -0
- package/src/analysis/constant.ts +310 -0
- package/src/analysis/document-order.ts +141 -0
- package/src/analysis/inlineability.ts +336 -0
- package/src/analysis/purity.ts +315 -0
- package/src/index.ts +2 -0
- package/src/obfuscator.ts +271 -0
- package/src/options.ts +114 -0
- package/src/preparation/dot-to-bracket.ts +41 -0
- package/src/preparation/expand-destructuring.ts +211 -0
- package/src/preparation/expand-template-literals.ts +92 -0
- package/src/preparation/rewrite-class-fields.ts +320 -0
- package/src/preparation/split-sequence-expressions.ts +40 -0
- package/src/preparation/split-variable-declarations.ts +39 -0
- package/src/preparation/stringify-object-keys.ts +107 -0
- package/src/preparation/wrap-single-statements.ts +63 -0
- package/src/transforms/arguments-to-parameters.ts +187 -0
- package/src/transforms/arrow-to-function.ts +225 -0
- package/src/transforms/collapse-single-use-temps.ts +957 -0
- package/src/transforms/const-let-to-var.ts +246 -0
- package/src/transforms/const-to-let.ts +38 -0
- package/src/transforms/do-while-to-while.ts +69 -0
- package/src/transforms/drop-console.ts +119 -0
- package/src/transforms/drop-debugger.ts +38 -0
- package/src/transforms/expand-binary-assignment.ts +94 -0
- package/src/transforms/expand-logical-assignment.ts +84 -0
- package/src/transforms/extract-object-properties.ts +404 -0
- package/src/transforms/fold-builtin-methods.ts +949 -0
- package/src/transforms/fold-constants.ts +462 -0
- package/src/transforms/for-to-while.ts +109 -0
- package/src/transforms/function-declaration-to-expression.ts +108 -0
- package/src/transforms/inline-functions.ts +1074 -0
- package/src/transforms/logical-to-ternary.ts +92 -0
- package/src/transforms/mangle-properties.ts +819 -0
- package/src/transforms/nullish-coalescing-to-ternary.ts +77 -0
- package/src/transforms/numbers-to-strings.ts +61 -0
- package/src/transforms/object-method-to-property.ts +86 -0
- package/src/transforms/optional-chaining-to-ternary.ts +225 -0
- package/src/transforms/pack-declarations-into-parameters.ts +233 -0
- package/src/transforms/remove-anonymous-function-names.ts +84 -0
- package/src/transforms/remove-unreachable-code.ts +115 -0
- package/src/transforms/remove-unused-code.ts +296 -0
- package/src/transforms/rename-identifiers.ts +289 -0
- package/src/transforms/specials-to-strings.ts +116 -0
- package/src/transforms/switch-to-if.ts +339 -0
- package/src/transforms/ternary-to-if.ts +387 -0
- package/src/transforms/update-to-assignment.ts +132 -0
- package/src/transforms/yodify-conditions.ts +103 -0
- package/src/utils/ast.ts +129 -0
- package/src/utils/change-tracking.ts +15 -0
- package/src/utils/dom-properties.ts +9252 -0
- package/src/utils/evaluation-order.ts +178 -0
- package/src/utils/literal.ts +96 -0
- package/src/utils/logical-lowering.ts +132 -0
- package/src/utils/loop-lowering.ts +109 -0
- package/src/utils/paths.ts +103 -0
- package/src/utils/random.ts +22 -0
- package/src/utils/string-generator.ts +206 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import traverse from '@babel/traverse'
|
|
2
|
+
import type { NodePath, Scope, Visitor } from '@babel/traverse'
|
|
3
|
+
import * as t from '@babel/types'
|
|
4
|
+
import type { File } from '@babel/types'
|
|
5
|
+
|
|
6
|
+
import { initializerReaches } from 'src/analysis/document-order'
|
|
7
|
+
import { enclosingScopeHasDirectEval, isInsideWith } from 'src/utils/ast'
|
|
8
|
+
import { hasConstantViolation } from 'src/utils/paths'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lifts safe lexical declarations to function-scoped `var`, renaming collisions
|
|
12
|
+
* caused by hoisting. TDZ reads, reassigned constants, captured loop bindings,
|
|
13
|
+
* uninitialized loop heads, program scope, `with`, and direct eval are refused.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // ◀️ before
|
|
17
|
+
* function f() {
|
|
18
|
+
* const x = 1;
|
|
19
|
+
* return x;
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* // ▶️ after
|
|
23
|
+
* function f() {
|
|
24
|
+
* var x = 1;
|
|
25
|
+
* return x;
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
export function constLetToVar(ast: File): boolean {
|
|
29
|
+
const state = { changed: false }
|
|
30
|
+
traverse(ast, {
|
|
31
|
+
VariableDeclaration(path) {
|
|
32
|
+
const { kind } = path.node
|
|
33
|
+
if (kind !== 'let' && kind !== 'const') {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
// Babel cannot see names supplied by `with` or direct eval.
|
|
37
|
+
if (isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
// A `var` write would lose the `const` TypeError.
|
|
41
|
+
if (kind === 'const' && hasConstantViolation(path)) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
// Hoisted `var` reads `undefined` where a lexical binding throws in TDZ.
|
|
45
|
+
if (hasTdzRiskyReference(path)) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
// An uninitialized lexical loop head resets on each entry. `var` does not.
|
|
49
|
+
if (forHeadMissingInitializer(path)) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
// Top-level `var` creates a global property. Lexical declarations do not.
|
|
53
|
+
const blockScope = path.scope
|
|
54
|
+
const varScope = blockScope.getFunctionParent() ?? blockScope.getProgramParent()
|
|
55
|
+
if (varScope.path.isProgram()) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
// One `var` cannot preserve per-iteration bindings captured by closures.
|
|
59
|
+
if (capturedInEnclosingLoop(path)) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
transformBlockScopedVariable(path, varScope)
|
|
63
|
+
state.changed = true
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
ClassDeclaration(path) {
|
|
67
|
+
const { id } = path.node
|
|
68
|
+
if (!id) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { scope } = path.parentPath
|
|
76
|
+
if (!isVarScope(scope) && scope.parent!.hasBinding(id.name, { noUids: true })) {
|
|
77
|
+
path.scope.rename(id.name)
|
|
78
|
+
state.changed = true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
return state.changed
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const conflictingFunctionsVisitor: Visitor<{ names: string[] }> = {
|
|
86
|
+
Scope(path, { names }) {
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const binding = path.scope.getOwnBinding(name)
|
|
89
|
+
if (binding?.kind === 'hoisted') {
|
|
90
|
+
path.scope.rename(name)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
Expression(path) {
|
|
95
|
+
path.skip()
|
|
96
|
+
},
|
|
97
|
+
Declaration(path) {
|
|
98
|
+
path.skip()
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Same-scope uses need a reachable initializer. Nested uses also need their
|
|
103
|
+
// function value created after initialization.
|
|
104
|
+
function hasTdzRiskyReference(path: NodePath<t.VariableDeclaration>): boolean {
|
|
105
|
+
const declHome = nearestFunctionOrProgram(path)
|
|
106
|
+
for (const name of Object.keys(path.getBindingIdentifiers())) {
|
|
107
|
+
const binding = path.scope.getBinding(name)
|
|
108
|
+
if (!binding) {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
for (const usage of [...binding.referencePaths, ...binding.constantViolations]) {
|
|
112
|
+
if (usage.findParent((ancestor) => ancestor === path)) {
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
const usageHome = nearestFunctionOrProgram(usage)
|
|
116
|
+
if (usageHome === declHome) {
|
|
117
|
+
if (!initializerReaches(path, usage)) {
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
const nested = outermostNestedFunction(usage, declHome)
|
|
123
|
+
if (!nested || nested.isFunctionDeclaration() || !initializerReaches(path, nested)) {
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function forHeadMissingInitializer(path: NodePath<t.VariableDeclaration>): boolean {
|
|
132
|
+
if (!t.isForStatement(path.parent) || path.key !== 'init') {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
return path.node.declarations.some((decl) => decl.init == null)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// A nested function or class can observe the loop's per-iteration binding.
|
|
139
|
+
function capturedInEnclosingLoop(path: NodePath<t.VariableDeclaration>): boolean {
|
|
140
|
+
const loop = enclosingLoop(path)
|
|
141
|
+
if (!loop) {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
for (const name of Object.keys(path.getBindingIdentifiers())) {
|
|
145
|
+
const binding = path.scope.getBinding(name)
|
|
146
|
+
if (!binding) {
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
for (const usage of [...binding.referencePaths, ...binding.constantViolations]) {
|
|
150
|
+
for (
|
|
151
|
+
let current = usage.parentPath;
|
|
152
|
+
current && current !== loop;
|
|
153
|
+
current = current.parentPath
|
|
154
|
+
) {
|
|
155
|
+
if (current.isFunction() || current.isClass()) {
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function transformBlockScopedVariable(
|
|
165
|
+
path: NodePath<t.VariableDeclaration>,
|
|
166
|
+
varScope: Scope,
|
|
167
|
+
): void {
|
|
168
|
+
path.node.kind = 'var'
|
|
169
|
+
|
|
170
|
+
const bindingNames = Object.keys(path.getBindingIdentifiers())
|
|
171
|
+
for (const name of bindingNames) {
|
|
172
|
+
const binding = path.scope.getOwnBinding(name)
|
|
173
|
+
if (!binding) {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
binding.kind = 'var'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (enclosingLoop(path) && !isVarInLoopHead(path)) {
|
|
180
|
+
for (const decl of path.node.declarations) {
|
|
181
|
+
// Without an initializer, `var` retains the prior iteration's value.
|
|
182
|
+
decl.init ??= t.buildUndefinedNode()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const blockScope = path.scope
|
|
187
|
+
if (varScope !== blockScope) {
|
|
188
|
+
for (const name of bindingNames) {
|
|
189
|
+
let newName = name
|
|
190
|
+
if (
|
|
191
|
+
blockScope.parent!.hasBinding(name, { noUids: true }) ||
|
|
192
|
+
blockScope.parent!.hasGlobal(name)
|
|
193
|
+
) {
|
|
194
|
+
newName = blockScope.generateUid(name)
|
|
195
|
+
blockScope.rename(name, newName)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
blockScope.moveBindingTo(newName, varScope)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
blockScope.path.traverse(conflictingFunctionsVisitor, {
|
|
203
|
+
names: bindingNames,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function nearestFunctionOrProgram(path: NodePath): NodePath {
|
|
208
|
+
return path.findParent((ancestor) => ancestor.isFunction() || ancestor.isProgram())!
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// The shallowest nested function decides when this use can run.
|
|
212
|
+
function outermostNestedFunction(usage: NodePath, home: NodePath): NodePath | null {
|
|
213
|
+
let result: NodePath | null = null
|
|
214
|
+
for (let current = usage.parentPath; current && current !== home; current = current.parentPath) {
|
|
215
|
+
if (current.isFunction()) {
|
|
216
|
+
result = current
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return result
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function enclosingLoop(path: NodePath): NodePath<t.Loop> | null {
|
|
223
|
+
for (let current = path.parentPath; current; current = current.parentPath) {
|
|
224
|
+
if (current.isFunction()) {
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
if (current.isLoop()) {
|
|
228
|
+
return current
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isVarScope(scope: Scope): boolean {
|
|
235
|
+
return scope.path.isFunctionParent() || scope.path.isProgram()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isVarInLoopHead(path: NodePath<t.VariableDeclaration>): boolean {
|
|
239
|
+
if (t.isForStatement(path.parent)) {
|
|
240
|
+
return path.key === 'init'
|
|
241
|
+
}
|
|
242
|
+
if (t.isForXStatement(path.parent)) {
|
|
243
|
+
return path.key === 'left'
|
|
244
|
+
}
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Visitor } from '@babel/traverse'
|
|
2
|
+
import type { File } from '@babel/types'
|
|
3
|
+
|
|
4
|
+
import { enclosingScopeHasDirectEval } from 'src/utils/ast'
|
|
5
|
+
import { traverseForChanges } from 'src/utils/change-tracking'
|
|
6
|
+
import type { ChangeState } from 'src/utils/change-tracking'
|
|
7
|
+
import { hasConstantViolation } from 'src/utils/paths'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Changes `const` to `let` when no write can observe the lost TypeError. Direct
|
|
11
|
+
* eval and recorded constant violations keep the declaration unchanged. Scope
|
|
12
|
+
* and TDZ behavior are otherwise identical.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // ◀️ before
|
|
16
|
+
* const foo = "bar";
|
|
17
|
+
* const { a, b } = obj;
|
|
18
|
+
*
|
|
19
|
+
* // ▶️ after
|
|
20
|
+
* let foo = "bar";
|
|
21
|
+
* let { a, b } = obj;
|
|
22
|
+
*/
|
|
23
|
+
export function constToLet(ast: File): boolean {
|
|
24
|
+
return traverseForChanges(ast, visitor)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const visitor: Visitor<ChangeState> = {
|
|
28
|
+
VariableDeclaration(path, state) {
|
|
29
|
+
if (path.node.kind !== 'const') {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
if (enclosingScopeHasDirectEval(path) || hasConstantViolation(path)) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
path.node.kind = 'let'
|
|
36
|
+
state.changed = true
|
|
37
|
+
},
|
|
38
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { NodePath, Visitor } from '@babel/traverse'
|
|
2
|
+
import * as t from '@babel/types'
|
|
3
|
+
import type { File } from '@babel/types'
|
|
4
|
+
|
|
5
|
+
import { traverseForChanges } from 'src/utils/change-tracking'
|
|
6
|
+
import type { ChangeState } from 'src/utils/change-tracking'
|
|
7
|
+
import { collectLoopLabels, generateLoopLabel, redirectContinues } from 'src/utils/loop-lowering'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rewrites `do` loops as `while (true)` with an exit test after the body.
|
|
11
|
+
* Continues become breaks from an inner block so they still reach that test.
|
|
12
|
+
* The inner label is omitted when nothing uses it.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // ◀️ before
|
|
16
|
+
* do {
|
|
17
|
+
* step();
|
|
18
|
+
* if (skip()) continue;
|
|
19
|
+
* log();
|
|
20
|
+
* } while (more());
|
|
21
|
+
*
|
|
22
|
+
* // ▶️ after
|
|
23
|
+
* while (true) {
|
|
24
|
+
* _doIteration: {
|
|
25
|
+
* step();
|
|
26
|
+
* if (skip()) break _doIteration;
|
|
27
|
+
* log();
|
|
28
|
+
* }
|
|
29
|
+
* if (!more()) break;
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
export function doWhileToWhile(ast: File): boolean {
|
|
33
|
+
return traverseForChanges(ast, visitor)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const visitor: Visitor<ChangeState> = {
|
|
37
|
+
DoWhileStatement(path, state) {
|
|
38
|
+
transformDoWhile(path)
|
|
39
|
+
state.changed = true
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function transformDoWhile(path: NodePath<t.DoWhileStatement>): void {
|
|
44
|
+
const { node } = path
|
|
45
|
+
const { ourLabels } = collectLoopLabels(path)
|
|
46
|
+
const innerLabel = generateLoopLabel(path, 'doIteration')
|
|
47
|
+
|
|
48
|
+
const blockBody: t.BlockStatement = t.isBlockStatement(node.body)
|
|
49
|
+
? node.body
|
|
50
|
+
: t.blockStatement([node.body])
|
|
51
|
+
const labeledBlock = t.labeledStatement(t.identifier(innerLabel), blockBody)
|
|
52
|
+
const exitTest = t.ifStatement(
|
|
53
|
+
t.unaryExpression('!', node.test),
|
|
54
|
+
t.blockStatement([t.breakStatement()]),
|
|
55
|
+
)
|
|
56
|
+
const whileStmt = t.whileStatement(
|
|
57
|
+
t.booleanLiteral(true),
|
|
58
|
+
t.blockStatement([labeledBlock, exitTest]),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const [newPath] = path.replaceWith(whileStmt)
|
|
62
|
+
const rewriteCount = redirectContinues(newPath, innerLabel, ourLabels)
|
|
63
|
+
|
|
64
|
+
// Keep the block so body bindings cannot shadow names in the exit test.
|
|
65
|
+
if (rewriteCount === 0) {
|
|
66
|
+
const whileBlock = newPath.node.body as t.BlockStatement
|
|
67
|
+
whileBlock.body = [blockBody, exitTest]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import traverse from '@babel/traverse'
|
|
2
|
+
import type { NodePath, Visitor } from '@babel/traverse'
|
|
3
|
+
import * as t from '@babel/types'
|
|
4
|
+
import type { File } from '@babel/types'
|
|
5
|
+
|
|
6
|
+
import { isSideEffectFree } from 'src/analysis/purity'
|
|
7
|
+
import { isInsideWith } from 'src/utils/ast'
|
|
8
|
+
import { traverseForChanges } from 'src/utils/change-tracking'
|
|
9
|
+
import type { ChangeState } from 'src/utils/change-tracking'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Removes `console.*(...)` expression statements when `console` is not
|
|
13
|
+
* locally shadowed. Matches dot, bracket, and optional-chain forms.
|
|
14
|
+
* Leaves indirect uses alone (`console.log.call(...)`, assignments,
|
|
15
|
+
* `await console.log(...)`).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // ◀️ before
|
|
19
|
+
* function foo() {
|
|
20
|
+
* console.log("hi");
|
|
21
|
+
* return "bar";
|
|
22
|
+
* }
|
|
23
|
+
* function bar(console) {
|
|
24
|
+
* console.log("kept");
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // ▶️ after
|
|
28
|
+
* function foo() {
|
|
29
|
+
* return "bar";
|
|
30
|
+
* }
|
|
31
|
+
* function bar(console) {
|
|
32
|
+
* console.log("kept");
|
|
33
|
+
* }
|
|
34
|
+
*/
|
|
35
|
+
export function dropConsole(ast: File): boolean {
|
|
36
|
+
// A reassigned global `console` can invoke user code.
|
|
37
|
+
if (globalConsoleIsReassigned(ast)) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
return traverseForChanges(ast, visitor)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function globalConsoleIsReassigned(ast: File): boolean {
|
|
44
|
+
let reassigned = false
|
|
45
|
+
traverse(ast, {
|
|
46
|
+
Identifier(path) {
|
|
47
|
+
if (path.node.name !== 'console' || reassigned) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
// Per-call checks already handle local shadows.
|
|
51
|
+
if (path.scope.getBinding('console')) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (isWriteTarget(path)) {
|
|
55
|
+
reassigned = true
|
|
56
|
+
path.stop()
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
return reassigned
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isWriteTarget(path: NodePath<t.Identifier>): boolean {
|
|
64
|
+
const { node, parentPath } = path
|
|
65
|
+
if (!parentPath) {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
if (parentPath.isAssignmentExpression({ left: node })) {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
if (parentPath.isUpdateExpression({ argument: node })) {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
// Destructuring and iterator-loop targets also write the name.
|
|
75
|
+
return path.isBindingIdentifier()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const visitor: Visitor<ChangeState> = {
|
|
79
|
+
ExpressionStatement(path, state) {
|
|
80
|
+
const expr = path.node.expression
|
|
81
|
+
if (!t.isCallExpression(expr) && !t.isOptionalCallExpression(expr)) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const { callee } = expr
|
|
85
|
+
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
const { object } = callee
|
|
89
|
+
if (!t.isIdentifier(object) || object.name !== 'console') {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
// `with` can supply a different `console` outside Babel's scope table.
|
|
93
|
+
if (path.scope.getBinding('console') || isInsideWith(path)) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
// Removing the call also removes argument and computed-key evaluation.
|
|
97
|
+
if (!callArgumentsAreInert(expr, callee, path)) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
path.remove()
|
|
101
|
+
state.changed = true
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function callArgumentsAreInert(
|
|
106
|
+
expr: t.CallExpression | t.OptionalCallExpression,
|
|
107
|
+
callee: t.MemberExpression | t.OptionalMemberExpression,
|
|
108
|
+
path: NodePath<t.ExpressionStatement>,
|
|
109
|
+
): boolean {
|
|
110
|
+
if (callee.computed && !isSideEffectFree(callee.property, path.scope, path)) {
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
for (const arg of expr.arguments) {
|
|
114
|
+
if (!t.isExpression(arg) || !isSideEffectFree(arg, path.scope, path)) {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Visitor } from '@babel/traverse'
|
|
2
|
+
import * as t from '@babel/types'
|
|
3
|
+
import type { File } from '@babel/types'
|
|
4
|
+
|
|
5
|
+
import { traverseForChanges } from 'src/utils/change-tracking'
|
|
6
|
+
import type { ChangeState } from 'src/utils/change-tracking'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Removes every `debugger` statement.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // ◀️ before
|
|
13
|
+
* function foo() {
|
|
14
|
+
* debugger;
|
|
15
|
+
* return "bar";
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* // ▶️ after
|
|
19
|
+
* function foo() {
|
|
20
|
+
* return "bar";
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
export function dropDebugger(ast: File): boolean {
|
|
24
|
+
return traverseForChanges(ast, visitor)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const visitor: Visitor<ChangeState> = {
|
|
28
|
+
DebuggerStatement(path, state) {
|
|
29
|
+
// Babel cannot remove a required single-statement child. Keep the slot valid
|
|
30
|
+
// with an empty statement.
|
|
31
|
+
if (Array.isArray(path.container)) {
|
|
32
|
+
path.remove()
|
|
33
|
+
} else {
|
|
34
|
+
path.replaceWith(t.emptyStatement())
|
|
35
|
+
}
|
|
36
|
+
state.changed = true
|
|
37
|
+
},
|
|
38
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { NodePath, Visitor } from '@babel/traverse'
|
|
2
|
+
import * as t from '@babel/types'
|
|
3
|
+
import type { File } from '@babel/types'
|
|
4
|
+
|
|
5
|
+
import { isInsideWith } from 'src/utils/ast'
|
|
6
|
+
import { traverseForChanges } from 'src/utils/change-tracking'
|
|
7
|
+
import type { ChangeState } from 'src/utils/change-tracking'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rewrites arithmetic and bitwise compound assignments as `LHS = LHS op RHS`.
|
|
11
|
+
* Logical compound assignments are handled by `expandLogicalAssignment`.
|
|
12
|
+
*
|
|
13
|
+
* Member objects and computed keys are cached so the read and write use the same
|
|
14
|
+
* evaluated receiver. The cache assignment stays on the outer write target,
|
|
15
|
+
* which evaluates before its right side.
|
|
16
|
+
*
|
|
17
|
+
* Compound and expanded forms use the same operator semantics, including BigInt.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // ◀️ before
|
|
21
|
+
* a += b;
|
|
22
|
+
* a **= 2;
|
|
23
|
+
* obj.x -= y;
|
|
24
|
+
* arr[idx()] |= v;
|
|
25
|
+
*
|
|
26
|
+
* // ▶️ after
|
|
27
|
+
* a = a + b;
|
|
28
|
+
* a = a ** 2;
|
|
29
|
+
* (_obj = obj).x = _obj.x - y;
|
|
30
|
+
* (_arr = arr)[_idx = idx()] = _arr[_idx] | v;
|
|
31
|
+
*/
|
|
32
|
+
export function expandBinaryAssignment(ast: File): boolean {
|
|
33
|
+
return traverseForChanges(ast, visitor)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const visitor: Visitor<ChangeState> = {
|
|
37
|
+
AssignmentExpression(path, state) {
|
|
38
|
+
if (lowerBinaryAssignment(path)) {
|
|
39
|
+
state.changed = true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const LOGICAL_COMPOUND = new Set(['||', '&&', '??'])
|
|
45
|
+
|
|
46
|
+
function lowerBinaryAssignment(path: NodePath<t.AssignmentExpression>): boolean {
|
|
47
|
+
const { operator, left, right } = path.node
|
|
48
|
+
if (operator === '=' || operator.length < 2) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
const op = operator.slice(0, -1)
|
|
52
|
+
if (LOGICAL_COMPOUND.has(op)) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const read = t.cloneNode(left, true)
|
|
57
|
+
|
|
58
|
+
if (t.isMemberExpression(left) && t.isMemberExpression(read)) {
|
|
59
|
+
// `with` can turn a bare object name into a getter or shadow a memo temp.
|
|
60
|
+
if (isInsideWith(path)) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
const { object } = left
|
|
64
|
+
if (t.isExpression(object)) {
|
|
65
|
+
const memo = path.scope.maybeGenerateMemoised(object)
|
|
66
|
+
if (memo) {
|
|
67
|
+
read.object = memo
|
|
68
|
+
left.object = t.assignmentExpression('=', t.cloneNode(memo), object)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (left.computed) {
|
|
73
|
+
const { property } = left
|
|
74
|
+
if (t.isExpression(property)) {
|
|
75
|
+
const memoProp = path.scope.maybeGenerateMemoised(property)
|
|
76
|
+
if (memoProp) {
|
|
77
|
+
read.property = memoProp
|
|
78
|
+
left.property = t.assignmentExpression('=', t.cloneNode(memoProp), property)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else if (!t.isIdentifier(left)) {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
path.replaceWith(
|
|
87
|
+
t.assignmentExpression(
|
|
88
|
+
'=',
|
|
89
|
+
left,
|
|
90
|
+
t.binaryExpression(op as t.BinaryExpression['operator'], read as t.Expression, right),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { NodePath, Visitor } from '@babel/traverse'
|
|
2
|
+
import * as t from '@babel/types'
|
|
3
|
+
import type { File } from '@babel/types'
|
|
4
|
+
|
|
5
|
+
import { isInsideWith } from 'src/utils/ast'
|
|
6
|
+
import { traverseForChanges } from 'src/utils/change-tracking'
|
|
7
|
+
import type { ChangeState } from 'src/utils/change-tracking'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rewrites `||=`/`&&=`/`??=` as `LHS op (LHS = RHS)`. Member objects and
|
|
11
|
+
* computed keys are cached so the read and write use the same receiver.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // ◀️ before
|
|
15
|
+
* a ||= b;
|
|
16
|
+
* a &&= b;
|
|
17
|
+
* a ??= b;
|
|
18
|
+
* obj.x ||= y;
|
|
19
|
+
* arr[idx()] ??= v;
|
|
20
|
+
*
|
|
21
|
+
* // ▶️ after
|
|
22
|
+
* a || (a = b);
|
|
23
|
+
* a && (a = b);
|
|
24
|
+
* a ?? (a = b);
|
|
25
|
+
* (_obj = obj).x || (_obj.x = y);
|
|
26
|
+
* (_arr = arr)[_idx = idx()] ?? (_arr[_idx] = v);
|
|
27
|
+
*/
|
|
28
|
+
export function expandLogicalAssignment(ast: File): boolean {
|
|
29
|
+
return traverseForChanges(ast, visitor)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const visitor: Visitor<ChangeState> = {
|
|
33
|
+
AssignmentExpression(path, state) {
|
|
34
|
+
if (lowerLogicalAssignment(path)) {
|
|
35
|
+
state.changed = true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function lowerLogicalAssignment(path: NodePath<t.AssignmentExpression>): boolean {
|
|
41
|
+
const { operator, left, right } = path.node
|
|
42
|
+
if (operator.length < 2) {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
const op = operator.slice(0, -1)
|
|
46
|
+
if (op !== '||' && op !== '&&' && op !== '??') {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const lhs = t.cloneNode(left, true)
|
|
51
|
+
|
|
52
|
+
if (t.isMemberExpression(left) && t.isMemberExpression(lhs)) {
|
|
53
|
+
// `with` can turn a bare object name into a getter or shadow a memo temp.
|
|
54
|
+
if (isInsideWith(path)) {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
const { object } = left
|
|
58
|
+
if (t.isExpression(object)) {
|
|
59
|
+
const memo = path.scope.maybeGenerateMemoised(object)
|
|
60
|
+
if (memo) {
|
|
61
|
+
left.object = memo
|
|
62
|
+
lhs.object = t.assignmentExpression('=', t.cloneNode(memo), object)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (left.computed) {
|
|
67
|
+
const { property } = left
|
|
68
|
+
if (t.isExpression(property)) {
|
|
69
|
+
const memoProp = path.scope.maybeGenerateMemoised(property)
|
|
70
|
+
if (memoProp) {
|
|
71
|
+
left.property = memoProp
|
|
72
|
+
lhs.property = t.assignmentExpression('=', t.cloneNode(memoProp), property)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else if (!t.isIdentifier(left)) {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
path.replaceWith(
|
|
81
|
+
t.logicalExpression(op, lhs as t.Expression, t.assignmentExpression('=', left, right)),
|
|
82
|
+
)
|
|
83
|
+
return true
|
|
84
|
+
}
|