js-confuser-vm 0.1.1 → 0.1.2

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.
Files changed (58) hide show
  1. package/README.md +242 -89
  2. package/dist/compiler.js +583 -208
  3. package/dist/disassembler.js +58 -8
  4. package/dist/runtime.js +93 -74
  5. package/dist/template.js +81 -76
  6. package/dist/transforms/bytecode/concealConstants.js +2 -2
  7. package/dist/transforms/bytecode/controlFlowFlattening.js +143 -25
  8. package/dist/transforms/bytecode/dispatcher.js +3 -3
  9. package/dist/transforms/bytecode/resolveRegisters.js +19 -4
  10. package/dist/transforms/bytecode/selfModifying.js +88 -21
  11. package/dist/transforms/bytecode/specializedOpcodes.js +6 -3
  12. package/dist/transforms/bytecode/stringConcealing.js +253 -75
  13. package/dist/utils/ast-utils.js +61 -0
  14. package/dist/utils/op-utils.js +1 -0
  15. package/package.json +7 -1
  16. package/.gitmodules +0 -4
  17. package/.prettierignore +0 -1
  18. package/CHANGELOG.md +0 -358
  19. package/babel-plugin-inline-runtime.cjs +0 -34
  20. package/babel.config.json +0 -23
  21. package/bench.ts +0 -146
  22. package/disassemble.ts +0 -12
  23. package/index.ts +0 -43
  24. package/jest-strip-types.js +0 -10
  25. package/jest.config.js +0 -64
  26. package/output.disassembled.js +0 -41
  27. package/src/build-runtime.ts +0 -113
  28. package/src/compiler.ts +0 -2703
  29. package/src/disassembler.ts +0 -329
  30. package/src/index.ts +0 -24
  31. package/src/minify.ts +0 -21
  32. package/src/options.ts +0 -24
  33. package/src/runtime.ts +0 -956
  34. package/src/template.ts +0 -265
  35. package/src/transforms/bytecode/aliasedOpcodes.ts +0 -151
  36. package/src/transforms/bytecode/concealConstants.ts +0 -52
  37. package/src/transforms/bytecode/controlFlowFlattening.ts +0 -566
  38. package/src/transforms/bytecode/dispatcher.ts +0 -292
  39. package/src/transforms/bytecode/macroOpcodes.ts +0 -193
  40. package/src/transforms/bytecode/resolveConstants.ts +0 -126
  41. package/src/transforms/bytecode/resolveLabels.ts +0 -112
  42. package/src/transforms/bytecode/resolveRegisters.ts +0 -226
  43. package/src/transforms/bytecode/selfModifying.ts +0 -121
  44. package/src/transforms/bytecode/specializedOpcodes.ts +0 -164
  45. package/src/transforms/bytecode/stringConcealing.ts +0 -130
  46. package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
  47. package/src/transforms/runtime/classObfuscation.ts +0 -59
  48. package/src/transforms/runtime/macroOpcodes.ts +0 -138
  49. package/src/transforms/runtime/minify.ts +0 -1
  50. package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
  51. package/src/transforms/runtime/specializedOpcodes.ts +0 -161
  52. package/src/types.ts +0 -134
  53. package/src/utils/ast-utils.ts +0 -19
  54. package/src/utils/op-utils.ts +0 -46
  55. package/src/utils/pass-utils.ts +0 -126
  56. package/src/utils/profile-utils.ts +0 -3
  57. package/src/utils/random-utils.ts +0 -31
  58. package/tsconfig.json +0 -12
@@ -1,130 +0,0 @@
1
- // String Concealing
2
- //
3
- // Encodes every string constant in each function with base64, then inserts a
4
- // decode closure (atob) that is called immediately after each LOAD_CONST to
5
- // recover the original value at runtime.
6
- //
7
- // ── How it works ─────────────────────────────────────────────────────────────
8
- //
9
- // Each function that contains at least one string LOAD_CONST gets:
10
- //
11
- // rClosure — a register holding the decode closure, created ONCE at function
12
- // entry (hoisted). All decode calls within the function reuse it.
13
- //
14
- // The decode function is compiled ONCE (shared across all functions) from a
15
- // Template:
16
- //
17
- // function decode(encoded) { return atob(encoded); }
18
- //
19
- // String constant transformations:
20
- //
21
- // Original: LOAD_CONST rDst, "hello"
22
- // Becomes: LOAD_CONST rDst, "aGVsbG8=" (base64-encoded)
23
- // CALL rDst, rClosure, 1, rDst (decode in-place)
24
- //
25
- // ── Pipeline position ─────────────────────────────────────────────────────────
26
- // Runs BEFORE resolveRegisters and resolveLabels (same slot as Dispatcher/CFF).
27
-
28
- import { Compiler } from "../../compiler.ts";
29
- import { Template } from "../../template.ts";
30
- import type { Bytecode, RegisterOperand } from "../../types.ts";
31
- import * as b from "../../types.ts";
32
- import { ref, buildMaxIdMap, allocReg, forEachFunction } from "../../utils/pass-utils.ts";
33
-
34
- // ── Per-function transformation ──────────────────────────────────────────────
35
-
36
- function processFunctionBlock(
37
- instrs: Bytecode,
38
- fnId: number,
39
- compiler: Compiler,
40
- maxId: Map<number, number>,
41
- decodeDesc: any,
42
- ): { instrs: Bytecode } {
43
- const OP = compiler.OP;
44
-
45
- // Only transform functions that contain string constants.
46
- const hasStringConst = instrs.some((instr) => {
47
- if (instr[0] !== OP.LOAD_CONST) return false;
48
- const operands = instr.slice(1);
49
- return (
50
- operands.length === 2 &&
51
- (operands[1] as any)?.type === "constant" &&
52
- typeof (operands[1] as any).value === "string"
53
- );
54
- });
55
- if (!hasStringConst) return { instrs };
56
-
57
- const rClosure = allocReg(fnId, maxId);
58
- const out: Bytecode = [];
59
-
60
- // Hoist: create the decode closure once at function entry.
61
- out.push([
62
- OP.MAKE_CLOSURE!,
63
- ref(rClosure),
64
- { type: "label", label: decodeDesc.entryLabel },
65
- decodeDesc.paramCount, // 1 (encoded)
66
- b.fnRegCountOperand(decodeDesc._fnIdx),
67
- 0, // no upvalues
68
- 0, // hasRest = false
69
- ]);
70
-
71
- // Transform each instruction.
72
- for (const instr of instrs) {
73
- if (
74
- instr[0] === OP.LOAD_CONST &&
75
- instr.length === 3 &&
76
- (instr[2] as any)?.type === "constant" &&
77
- typeof (instr[2] as any).value === "string"
78
- ) {
79
- const dst = instr[1] as RegisterOperand;
80
- const constOp = instr[2] as any;
81
-
82
- // Encode the string in-place.
83
- constOp.value = Buffer.from(constOp.value as string, "utf-8").toString(
84
- "base64",
85
- );
86
-
87
- out.push(instr);
88
-
89
- // Decode: rDst = decode(rDst)
90
- out.push([
91
- OP.CALL!,
92
- ref(dst), // dst — receives decoded string
93
- ref(rClosure), // the hoisted decode closure
94
- 1, // argc
95
- ref(dst), // arg[0] = encoded value
96
- ]);
97
- } else {
98
- out.push(instr);
99
- }
100
- }
101
-
102
- return { instrs: out };
103
- }
104
-
105
- // ── Pass entry point ──────────────────────────────────────────────────────────
106
- export function stringConcealing(
107
- bc: Bytecode,
108
- compiler: Compiler,
109
- ): { bytecode: Bytecode } {
110
- const maxId = buildMaxIdMap(bc);
111
-
112
- // Compile the decode function ONCE — all functions share the same closure body.
113
- const decodeTemplate = new Template(`
114
- function decode(encoded) {
115
- return atob(encoded);
116
- }
117
- `).compile({}, compiler);
118
- const decodeDesc = decodeTemplate.functions[0];
119
-
120
- const { bytecode } = forEachFunction(bc, compiler, (fnInstrs, fnId) =>
121
- processFunctionBlock(fnInstrs, fnId, compiler, maxId, decodeDesc),
122
- );
123
-
124
- // Append the decode function's bytecode at the end (defines its entryLabel).
125
- // This is a single shared closure, not per-function, so it lives outside
126
- // forEachFunction's tail mechanism.
127
- bytecode.push(...decodeTemplate.bytecode);
128
-
129
- return { bytecode };
130
- }
@@ -1,191 +0,0 @@
1
- import * as t from "@babel/types";
2
- import traverseImport from "@babel/traverse";
3
- import { ok } from "assert";
4
- import { Compiler } from "../../compiler.ts";
5
-
6
- const traverse = (traverseImport.default ||
7
- traverseImport) as typeof traverseImport.default;
8
-
9
- // Extract the real statement list from a SwitchCase consequent.
10
- function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
11
- let stmts: t.Statement[];
12
- if (
13
- switchCase.consequent.length === 1 &&
14
- t.isBlockStatement(switchCase.consequent[0])
15
- ) {
16
- stmts = (switchCase.consequent[0] as t.BlockStatement).body;
17
- } else {
18
- stmts = switchCase.consequent as t.Statement[];
19
- }
20
- return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
21
- }
22
-
23
- // Replace every `this._operand()` call in bodyStmts with `_operands[i]`
24
- // where i is the call's sequential index (0-based).
25
- // Returns the number of replacements performed.
26
- function replaceOperandCalls(bodyStmts: t.Statement[]): number {
27
- let replaced = 0;
28
-
29
- traverse(t.blockStatement(bodyStmts), {
30
- noScope: true,
31
- CallExpression(path) {
32
- const callee = path.node.callee;
33
-
34
- const isMethodCall = (methodName) => {
35
- return (
36
- t.isMemberExpression(callee) &&
37
- t.isThisExpression(callee.object) &&
38
- t.isIdentifier(callee.property, { name: methodName }) &&
39
- path.node.arguments.length === 0
40
- );
41
- };
42
-
43
- // Replace with _operands[i]
44
- const createOperandAccess = () => {
45
- return t.memberExpression(
46
- t.identifier("_operands"),
47
- t.numericLiteral(replaced++),
48
- true, // computed
49
- );
50
- };
51
-
52
- if (isMethodCall("_operand")) {
53
- path.replaceWith(createOperandAccess());
54
- }
55
-
56
- if (isMethodCall("_constant")) {
57
- path.node.arguments = [createOperandAccess(), createOperandAccess()];
58
- }
59
- },
60
- });
61
-
62
- return replaced;
63
- }
64
-
65
- // Appends a generated switch case for every entry in compiler.ALIASED_OPS.
66
- // Each alias case:
67
- // 1. Reads all operands eagerly into `_unsortedOperands` (in the shuffled
68
- // bytecode order) via sequential this._operand() calls.
69
- // 2. Restores the original operand order into `_operands` using the INVERSE
70
- // of the stored `order` permutation:
71
- // inverseOrder[order[i]] = i
72
- // _operands[j] = _unsortedOperands[inverseOrder[j]]
73
- // This is necessary because the bytecode stored originalOperands[order[i]]
74
- // at slot i, so recovering originalOperands[j] requires the inverse lookup.
75
- // 3. Executes a clone of the original handler body where every
76
- // this._operand() has been replaced by the corresponding `_operands[i]`.
77
- //
78
- // Must run AFTER applyMacroOpcodes / applySpecializedOpcodes (so original
79
- // cases already exist) but BEFORE applyShuffleOpcodes (so the new alias
80
- // cases are also shuffled into the handler order).
81
- export function applyAliasedOpcodes(ast: t.File, compiler: Compiler): void {
82
- if (!compiler.ALIASED_OPS || Object.keys(compiler.ALIASED_OPS).length === 0)
83
- return;
84
-
85
- let switchStatement: t.SwitchStatement | null = null;
86
- traverse(ast, {
87
- SwitchStatement(path) {
88
- if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
89
- switchStatement = path.node;
90
- path.stop();
91
- }
92
- },
93
- });
94
-
95
- ok(switchStatement, "Could not find @SWITCH statement for aliased opcodes");
96
-
97
- // Build opName → SwitchCase map from existing OP.xxx case tests.
98
- const nameToCaseMap = new Map<string, t.SwitchCase>();
99
- for (const sc of (switchStatement as t.SwitchStatement).cases) {
100
- const test = sc.test;
101
- if (
102
- test &&
103
- t.isMemberExpression(test) &&
104
- t.isIdentifier(test.object, { name: "OP" }) &&
105
- t.isIdentifier(test.property)
106
- ) {
107
- nameToCaseMap.set((test.property as t.Identifier).name, sc);
108
- }
109
- }
110
-
111
- for (const [aliasOpStr, info] of Object.entries(compiler.ALIASED_OPS)) {
112
- const aliasOpCode = Number(aliasOpStr);
113
- const { originalOp, order } = info;
114
- const arity = order.length;
115
-
116
- const originalName = compiler.OP_NAME[originalOp];
117
- if (!originalName) continue;
118
-
119
- const originalCase = nameToCaseMap.get(originalName);
120
- if (!originalCase) continue;
121
-
122
- // Clone the original handler body (deep clone so we don't mutate the source)
123
- const bodyStmts: t.Statement[] = extractCaseBody(originalCase).map(
124
- (s) => t.cloneNode(s, true) as t.Statement,
125
- );
126
-
127
- // Replace this._operand() calls with _operands[i]
128
- const replaced = replaceOperandCalls(bodyStmts);
129
-
130
- // If the handler has a different number of _operand() calls than our
131
- // recorded arity, skip this alias (variable-operand handler guard).
132
- if (replaced !== arity) continue;
133
-
134
- // Build: var _unsortedOperands = [this._operand(), this._operand(), ...]
135
- // Reads operands in the NEW (shuffled) bytecode order.
136
- const unsortedInit = t.variableDeclaration("let", [
137
- t.variableDeclarator(
138
- t.identifier("_unsortedOperands"),
139
- t.arrayExpression(
140
- Array.from({ length: arity }, () =>
141
- t.callExpression(
142
- t.memberExpression(t.thisExpression(), t.identifier("_operand")),
143
- [],
144
- ),
145
- ),
146
- ),
147
- ),
148
- ]);
149
-
150
- // The inverse permutation maps original position j → unsorted index i,
151
- // because the bytecode stored originalOperands[order[i]] at slot i.
152
- // inverseOrder[j] = i means: original operand j lives at _unsortedOperands[i]
153
- const inverseOrder = new Array<number>(arity);
154
- for (let i = 0; i < arity; i++) {
155
- inverseOrder[order[i]] = i;
156
- }
157
-
158
- // Build: var _operands = [_unsortedOperands[inverseOrder[0]], ...]
159
- // Restores the original operand order expected by the handler body.
160
- const operandsInit = t.variableDeclaration("let", [
161
- t.variableDeclarator(
162
- t.identifier("_operands"),
163
- t.arrayExpression(
164
- inverseOrder.map((idx) =>
165
- t.memberExpression(
166
- t.identifier("_unsortedOperands"),
167
- t.numericLiteral(idx),
168
- true, // computed
169
- ),
170
- ),
171
- ),
172
- ),
173
- ]);
174
-
175
- const allStmts: t.Statement[] = [unsortedInit, operandsInit, ...bodyStmts];
176
-
177
- // Add a leading comment for readability in non-minified output
178
- t.addComment(
179
- allStmts[0],
180
- "leading",
181
- ` ${compiler.OP_NAME[aliasOpCode]} (order: [${order.join(",")}])`,
182
- true,
183
- );
184
-
185
- allStmts.push(t.breakStatement());
186
-
187
- (switchStatement as t.SwitchStatement).cases.push(
188
- t.switchCase(t.numericLiteral(aliasOpCode), [t.blockStatement(allStmts)]),
189
- );
190
- }
191
- }
@@ -1,59 +0,0 @@
1
- import { Compiler } from "../../compiler.ts";
2
- import * as t from "@babel/types";
3
- import { shuffle } from "../../utils/random-utils.ts";
4
-
5
- function hasComment(node: t.Node, text: string): boolean {
6
- const all = [
7
- ...((node as any).leadingComments ?? []),
8
- ...((node as any).innerComments ?? []),
9
- ...((node as any).trailingComments ?? []),
10
- ];
11
- return all.some((c) => c.value.includes(text));
12
- }
13
-
14
- function isPrototypeAssignment(stmt: t.Statement): boolean {
15
- if (!t.isExpressionStatement(stmt)) return false;
16
- const expr = stmt.expression;
17
- if (!t.isAssignmentExpression(expr)) return false;
18
- const left = expr.left;
19
- return (
20
- t.isMemberExpression(left) &&
21
- t.isMemberExpression(left.object) &&
22
- t.isIdentifier((left.object as t.MemberExpression).property, {
23
- name: "prototype",
24
- })
25
- );
26
- }
27
-
28
- export function applyClassObfuscation(ast: t.File, _compiler: Compiler): void {
29
- const body = ast.program.body;
30
-
31
- // Split at the first statement that carries the @BOOT comment.
32
- // Everything from that statement onward is the boot section and must stay last.
33
- let bootIdx = body.findIndex((stmt) => hasComment(stmt, "@BOOT"));
34
- if (bootIdx === -1) bootIdx = body.length;
35
-
36
- const shufflable = body.slice(0, bootIdx);
37
- const boot = body.slice(bootIdx);
38
-
39
- // Partition the shufflable section into two independent groups.
40
- // Group A: variable/function declarations (constructors, standalone vars).
41
- // Group B: prototype method assignments (X.prototype.Y = ...).
42
- // Both groups are shuffled independently; A always precedes B so that
43
- // constructors are defined before methods reference them.
44
- const varDecls: t.Statement[] = [];
45
- const methodDefs: t.Statement[] = [];
46
-
47
- for (const stmt of shufflable) {
48
- if (isPrototypeAssignment(stmt)) {
49
- methodDefs.push(stmt);
50
- } else {
51
- varDecls.push(stmt);
52
- }
53
- }
54
-
55
- shuffle(varDecls);
56
- shuffle(methodDefs);
57
-
58
- ast.program.body = [...varDecls, ...methodDefs, ...boot];
59
- }
@@ -1,138 +0,0 @@
1
- import * as t from "@babel/types";
2
- import traverseImport from "@babel/traverse";
3
- import { ok } from "assert";
4
- import { Compiler } from "../../compiler.ts";
5
- import generate from "@babel/generator";
6
- const traverse = (traverseImport.default ||
7
- traverseImport) as typeof traverseImport.default;
8
-
9
- // Extract the real statement list from a SwitchCase consequent, normalising
10
- // the two forms that appear in the runtime:
11
- // • A single wrapping BlockStatement → use its .body
12
- // • Statements listed directly → use as-is
13
- // In both cases trailing BreakStatement / EmptyStatement are filtered out.
14
- function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
15
- let stmts: t.Statement[];
16
- if (
17
- switchCase.consequent.length === 1 &&
18
- t.isBlockStatement(switchCase.consequent[0])
19
- ) {
20
- stmts = (switchCase.consequent[0] as t.BlockStatement).body;
21
- } else {
22
- stmts = switchCase.consequent as t.Statement[];
23
- }
24
- return stmts.filter((s) => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
25
- }
26
-
27
- export function getOpcodeToCaseMap(
28
- switchStatement: t.SwitchStatement,
29
- compiler: Compiler,
30
- ): Map<number, t.SwitchCase> {
31
- // Build a map opName → SwitchCase from the existing OP.xxx case tests.
32
- const opcodeToCaseMap = new Map<number, t.SwitchCase>();
33
- for (const sc of (switchStatement as t.SwitchStatement).cases) {
34
- const test = sc.test;
35
- if (!test) continue;
36
-
37
- let opcode;
38
- let opName;
39
- if (
40
- t.isMemberExpression(test) &&
41
- t.isIdentifier(test.object, { name: "OP" }) &&
42
- t.isIdentifier(test.property)
43
- ) {
44
- opName = test.property.name;
45
- opcode = +Object.keys(compiler.OP_NAME).find(
46
- (key) => compiler.OP_NAME[key] == opName,
47
- );
48
- } else if (t.isNumericLiteral(test)) {
49
- opcode = test.value;
50
- }
51
-
52
- ok(
53
- typeof opcode === "number" && !Number.isNaN(opcode),
54
- `Failed to parse ${opcode} from ${opName}`,
55
- );
56
- if (opcode !== undefined) {
57
- opcodeToCaseMap.set(opcode, sc);
58
- }
59
- }
60
-
61
- return opcodeToCaseMap;
62
- }
63
-
64
- // Append a generated switch case for every entry in compiler.MACRO_OPS.
65
- // Each case inlines the constituent case bodies directly — no operand stack,
66
- // no substitution needed. Because every opcode handler now reads its own
67
- // operands via this._operand(), those calls naturally consume the inline
68
- // operands that macroOpcodes.ts embedded on the macro instruction.
69
- // Must be called BEFORE applyShuffleOpcodes so the new cases get shuffled.
70
- export function applyMacroOpcodes(ast: t.File, compiler: Compiler): void {
71
- let switchStatement: t.SwitchStatement | null = null;
72
- traverse(ast, {
73
- SwitchStatement(path) {
74
- if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
75
- switchStatement = path.node;
76
- path.stop();
77
- }
78
- },
79
- });
80
-
81
- ok(switchStatement, "Could not find @SWITCH statement for macro opcodes");
82
-
83
- const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
84
-
85
- for (const [macroOpStr, constituentOps] of Object.entries(
86
- compiler.MACRO_OPS,
87
- )) {
88
- const macroOpCode = Number(macroOpStr);
89
- const N = constituentOps.length;
90
-
91
- // Resolve each constituent op value → case node via OP_NAME lookup.
92
- const constituentCases: t.SwitchCase[] = [];
93
- let allFound = true;
94
- for (const opVal of constituentOps) {
95
- const found = opcodeToCaseMap.get(opVal);
96
- if (!found) {
97
- allFound = false;
98
- break;
99
- }
100
- constituentCases.push(found);
101
- }
102
- if (!allFound) {
103
- throw new Error(
104
- `Could not find all constituent ops for macro op ${macroOpCode}`,
105
- );
106
- }
107
-
108
- const opNames = constituentOps.map((v) => compiler.OP_NAME[v] ?? `OP_${v}`);
109
- let newName = opNames.join(",");
110
- compiler.OP_NAME[macroOpCode] = newName;
111
-
112
- // ── Build the macro case body ──────────────────────────────────────────
113
- // Clone and inline each sub-instruction's case body directly.
114
- // No operand substitution needed: each body already calls this._operand()
115
- // to read its own operands, which will consume the inline operands that
116
- // macroOpcodes.ts embedded on the macro instruction in order.
117
- const bodyStmts: t.Statement[] = [];
118
-
119
- for (let i = 0; i < N; i++) {
120
- const subStmts = extractCaseBody(constituentCases[i]).map(
121
- (s) => t.cloneNode(s, true) as t.Statement,
122
- );
123
-
124
- if (subStmts.length > 0) {
125
- t.addComment(subStmts[0], "leading", ` ${opNames[i]}`, true);
126
- bodyStmts.push(...subStmts);
127
- }
128
- }
129
-
130
- bodyStmts.push(t.breakStatement());
131
-
132
- (switchStatement as t.SwitchStatement).cases.push(
133
- t.switchCase(t.numericLiteral(macroOpCode), [
134
- t.blockStatement(bodyStmts),
135
- ]),
136
- );
137
- }
138
- }
@@ -1 +0,0 @@
1
- export { minify as applyMinify } from "../../minify.ts";
@@ -1,24 +0,0 @@
1
- import * as t from "@babel/types";
2
- import traverseImport from "@babel/traverse";
3
- import { ok } from "assert";
4
- import { shuffle } from "../../utils/random-utils.ts";
5
- const traverse = (traverseImport.default ||
6
- traverseImport) as typeof traverseImport.default;
7
-
8
- // Randomly reorder the switch cases inside the @SWITCH statement so the
9
- // opcode handler order varies per build.
10
- export function applyShuffleOpcodes(ast: t.File): void {
11
- let switchStatement: t.SwitchStatement | null = null;
12
- traverse(ast, {
13
- SwitchStatement(path) {
14
- if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
15
- switchStatement = path.node;
16
- path.stop();
17
- }
18
- },
19
- });
20
-
21
- ok(switchStatement, "Could not find opcode handlers switch statement");
22
-
23
- switchStatement.cases = shuffle(switchStatement.cases);
24
- }