Skip to content

Commit a89ab45

Browse files
committed
feat: migrate void-to-undefined to compiled matchers
1 parent f1dc2f1 commit a89ab45

File tree

7 files changed

+45
-31
lines changed

7 files changed

+45
-31
lines changed

packages/matchers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"type": "module",
6+
"main": "src/index.ts",
67
"scripts": {
78
"build": "tsx scripts/build.ts",
89
"bench": "tsx benchmark/test.js",

packages/matchers/src/compiler.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,31 +46,38 @@ if (${checks ?? true}) { return captures; }`;
4646
*/
4747
export function compileVisitor<T extends t.Node>(
4848
schema: NodeSchema<T>,
49-
cb: (path: NodePath<T>, captures: object) => void,
5049
phase: 'enter' | 'exit' = 'enter',
51-
): Visitor {
50+
): <S = unknown>(cb: VisitNodeFunction<S, T>) => Visitor<S> {
5251
const context: Context = { captures: [] };
5352
const checks = compileNode(schema, 'node', context, false);
5453
const code = `
54+
const node = path.node;
5555
const captures = { ${context.captures.map((v) => `${v}: undefined`).join(', ')} };
56-
if (${checks ?? true}) { cb(path, captures); }`;
56+
if (${checks ?? true}) { cb.call(state, path, state, captures); }`;
5757

5858
// eslint-disable-next-line @typescript-eslint/no-implied-eval
59-
const match = Function('path', 'node', 'cb', code) as (
59+
const match = Function('path', 'state', 'cb', code) as (
6060
path: NodePath<T>,
61-
node: T,
62-
cb: (path: NodePath<T>, captures: object) => void,
61+
state: unknown,
62+
cb: VisitNodeFunction<unknown, T>,
6363
) => void;
6464

65-
return {
65+
return <S>(cb: VisitNodeFunction<S, T>) => ({
6666
[schema.type]: {
67-
[phase](path: NodePath<T>) {
68-
match(path, path.node, cb);
67+
[phase](path: NodePath<T>, state: S) {
68+
match(path, state, cb as never);
6969
},
7070
},
71-
};
71+
});
7272
}
7373

74+
export type VisitNodeFunction<S, T extends t.Node> = (
75+
this: S,
76+
path: NodePath<T>,
77+
state: S,
78+
captures: object,
79+
) => void;
80+
7481
type CompileFunction = (
7582
schema: never,
7683
input: string,

packages/matchers/test/visitor.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@ import * as m from '../src';
55

66
test('visitor', () => {
77
const ast = t.program([t.emptyStatement(), t.returnStatement()]);
8-
const cb = vi.fn((path: NodePath<t.ReturnStatement>, captures: object) => {
8+
const cb = vi.fn(function (
9+
this: undefined,
10+
path: NodePath<t.ReturnStatement>,
11+
state: unknown,
12+
captures: object,
13+
) {
914
expect(path.type).toBe('ReturnStatement');
15+
expect(this).toBe(state);
16+
expect(this).toBeUndefined();
1017
expect(captures).toEqual({});
1118
});
12-
const visitor = m.compileVisitor(m.returnStatement(), cb);
13-
traverse(ast, visitor);
19+
const visitor = m.compileVisitor(m.returnStatement());
20+
traverse(ast, visitor(cb));
1421

1522
expect(cb).toHaveBeenCalledOnce();
1623
});

packages/webcrack/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"@types/debug": "^4.1.12",
7575
"@types/node": "^22.13.16",
7676
"@webcrack/eslint-config": "workspace:*",
77+
"@webcrack/matchers": "workspace:*",
7778
"@webcrack/typescript-config": "workspace:*",
7879
"esbuild": "^0.25.2",
7980
"typescript": "^5.8.2"

packages/webcrack/src/unminify/test/void-to-undefined.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const expectJS = testTransform(voidToUndefined);
66

77
test('void 0', () => expectJS('void 0').toMatchInlineSnapshot('undefined;'));
88

9+
test('void 1', () => expectJS('void 1').toMatchInlineSnapshot('undefined;'));
10+
911
test('ignore when undefined is declared in scope', () =>
1012
expectJS('let undefined = 1; { void 0; }').toMatchInlineSnapshot(`
1113
let undefined = 1;
Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
import * as t from '@babel/types';
2-
import * as m from '@codemod/matchers';
2+
import * as m from '@webcrack/matchers';
33
import type { Transform } from '../../ast-utils';
44

5+
const visitor = m.compileVisitor(m.unaryExpression('void', m.numericLiteral()));
6+
57
export default {
68
name: 'void-to-undefined',
79
tags: ['safe'],
810
scope: true,
9-
visitor: () => {
10-
const matcher = m.unaryExpression('void', m.numericLiteral(0));
11-
return {
12-
UnaryExpression: {
13-
exit(path) {
14-
if (
15-
matcher.match(path.node) &&
16-
!path.scope.hasBinding('undefined', { noGlobals: true })
17-
) {
18-
path.replaceWith(t.identifier('undefined'));
19-
this.changes++;
20-
}
21-
},
22-
},
23-
};
24-
},
11+
visitor: () =>
12+
visitor((path, state) => {
13+
if (!path.scope.hasBinding('undefined', { noGlobals: true })) {
14+
path.replaceWith(t.identifier('undefined'));
15+
state.changes++;
16+
}
17+
}),
2518
} satisfies Transform;

pnpm-lock.yaml

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)