Skip to content

Commit 906447e

Browse files
committed
feat(evasive-transform): expose createEvasiveTransformPass for use with @endo/parser-pipeline
This exposes a new function, `createEvasiveTransformPass()`, which can be used with `@endo/parser-pipeline` to unify AST parsing. Refactored out some common code to maximize logic reuse. Both the new function and the existing `transformAst()` use the new `makeTransformCommentsVisitor()` to build the visitors for `@babel/traverse`.
1 parent 2dfaa5a commit 906447e

11 files changed

Lines changed: 202 additions & 60 deletions

File tree

.changeset/itchy-meteors-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@endo/evasive-transform': minor
3+
---
4+
5+
Expose `createEvasiveTransformPass()` for use with `@endo/parser-pipeline`.

packages/evasive-transform/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"devDependencies": {
4141
"@babel/types": "~7.28.2",
4242
"@endo/ses-ava": "workspace:^",
43+
"@types/babel__generator": "~7.27.0",
4344
"ava": "catalog:dev",
4445
"c8": "catalog:dev",
4546
"eslint": "catalog:dev",
@@ -74,6 +75,7 @@
7475
"dependencies": {
7576
"@babel/generator": "^7.28.3",
7677
"@babel/parser": "~7.28.3",
77-
"@babel/traverse": "~7.28.3"
78+
"@babel/traverse": "~7.28.3",
79+
"@types/babel__traverse": "~7.28.0"
7880
}
7981
}

packages/evasive-transform/src/generate.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,13 @@ const generator = /** @type {typeof import('@babel/generator')['default']} */ (
1717
babelGenerator.default || babelGenerator
1818
);
1919

20-
/**
21-
* @typedef {NonNullable<import('@babel/generator').GeneratorOptions['inputSourceMap']>} SourceMapOption
22-
*/
23-
2420
/**
2521
* Options for {@link generateCode} with source map
2622
*
2723
* @typedef GenerateAstOptionsWithSourceMap
2824
* @property {string} [source]
2925
* @property {string} sourceUrl - If present, we will generate a source map
30-
* @property {SourceMapOption | undefined} [sourceMap] - If present, the generated source map will be a transform over the given source map.
26+
* @property {object | string | undefined} [sourceMap] - If present, the generated source map will be a transform over the given source map.
3127
* @internal
3228
*/
3329

@@ -58,7 +54,7 @@ const generator = /** @type {typeof import('@babel/generator')['default']} */ (
5854

5955
/**
6056
* Generates new code from a Babel AST; returns code and source map
61-
*@overload
57+
* @overload
6258
* @param {import('@babel/types').File} ast - Babel "File" AST
6359
* @param {GenerateAstOptionsWithSourceMap} options - Options for the transform
6460
* @returns {TransformedResultWithSourceMap}
@@ -67,7 +63,7 @@ const generator = /** @type {typeof import('@babel/generator')['default']} */ (
6763

6864
/**
6965
* Generates new code from a Babel AST; returns code only
70-
*@overload
66+
* @overload
7167
* @param {import('@babel/types').File} ast - Babel "File" AST
7268
* @param {GenerateAstOptions} [options] - Options for the transform
7369
* @returns {TransformedResult}
@@ -93,6 +89,7 @@ export const generate = (ast, options) => {
9389
{
9490
sourceFileName: sourceUrl,
9591
sourceMaps: Boolean(sourceUrl),
92+
// @ts-expect-error undocumented option
9693
inputSourceMap,
9794
retainLines: true,
9895
...(source === undefined ? {} : { experimental_preserveFormat: true }),

packages/evasive-transform/src/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
/**
88
* @import {TransformedResult, TransformedResultWithSourceMap} from './generate.js'
9-
* @import {SourceMapOption} from './generate.js'
109
*/
1110

1211
import { transformAst } from './transform-ast.js';
@@ -17,7 +16,7 @@ import { generate } from './generate.js';
1716
* Options for {@link evadeCensorSync}
1817
*
1918
* @typedef EvadeCensorOptions
20-
* @property {SourceMapOption | undefined} [sourceMap] - Original source map in JSON string or object form
19+
* @property {object | string | undefined} [sourceMap] - Original source map in JSON string or object form
2120
* @property {string | undefined} [sourceUrl] - URL or filepath of the original source in `code`
2221
* @property {boolean | undefined} [elideComments] - Empties the comments but preserves interior newlines.
2322
* @property {import('./parse-ast.js').SourceType | undefined} [sourceType] - Module source type
@@ -128,3 +127,5 @@ export function evadeCensorSync(source, options) {
128127
export async function evadeCensor(source, options) {
129128
return evadeCensorSync(source, options);
130129
}
130+
131+
export { createEvasiveTransformPass } from './transform-pass.js';

packages/evasive-transform/src/transform-ast.js

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import {
1313
evadeRegexpLiteral,
1414
} from './transform-code.js';
1515

16+
/**
17+
* @import {Visitor} from '@babel/traverse'
18+
* @import {Comment, CommentBlock, CommentLine, File} from '@babel/types'
19+
*/
20+
1621
// TODO The following is sufficient on Node.js, but for compatibility with
1722
// `node -r esm`, we must use the pattern below.
1823
// Restore after https://github.com/Agoric/agoric-sdk/issues/8671.
@@ -38,13 +43,47 @@ const traverse = /** @type {typeof import('@babel/traverse')['default']} */ (
3843
* @property {boolean} [onlyComments]
3944
*/
4045

46+
/**
47+
* Options for {@link makeTransformCommentsVisitor}
48+
*
49+
* @internal
50+
* @typedef TransformCommentsVisitorOptions
51+
* @property {(node: Comment|CommentBlock|CommentLine) => void} transformComment
52+
* @property {boolean} [onlyComments]
53+
*/
54+
55+
/**
56+
* @param {TransformCommentsVisitorOptions} options
57+
* @returns {Visitor}
58+
*/
59+
export const makeTransformCommentsVisitor = ({
60+
transformComment,
61+
onlyComments = false,
62+
}) => ({
63+
enter(p) {
64+
const { leadingComments, innerComments, trailingComments } = p.node;
65+
if ('comments' in p.node) {
66+
(p.node.comments || []).forEach(node => transformComment(node));
67+
}
68+
leadingComments?.forEach(node => transformComment(node));
69+
innerComments?.forEach(node => transformComment(node));
70+
trailingComments?.forEach(node => transformComment(node));
71+
if (!onlyComments) {
72+
evadeStrings(p);
73+
evadeTemplates(p);
74+
evadeRegexpLiteral(p);
75+
evadeDecrementGreater(p);
76+
}
77+
},
78+
});
79+
4180
/**
4281
* Performs transformations on the given AST
4382
*
4483
* This function mutates `ast`.
4584
*
4685
* @internal
47-
* @param {import('@babel/types').File} ast - AST, as generated by Babel
86+
* @param {File} ast - AST, as generated by Babel
4887
* @param {TransformAstOptions} [opts]
4988
* @returns {void}
5089
*/
@@ -53,26 +92,9 @@ export function transformAst(
5392
{ elideComments = false, onlyComments = false } = {},
5493
) {
5594
const transformComment = elideComments ? elideComment : evadeComment;
56-
traverse(ast, {
57-
enter(p) {
58-
const { leadingComments, innerComments, trailingComments, type } = p.node;
59-
// discriminated union
60-
if ('comments' in p.node) {
61-
(p.node.comments || []).forEach(node => transformComment(node));
62-
}
63-
// Rewrite all comments.
64-
(leadingComments || []).forEach(node => transformComment(node));
65-
if (type.startsWith('Comment')) {
66-
transformComment(p.node);
67-
}
68-
(innerComments || []).forEach(node => transformComment(node));
69-
(trailingComments || []).forEach(node => transformComment(node));
70-
if (!onlyComments) {
71-
evadeStrings(p);
72-
evadeTemplates(p);
73-
evadeRegexpLiteral(p);
74-
evadeDecrementGreater(p);
75-
}
76-
},
95+
const visitor = makeTransformCommentsVisitor({
96+
transformComment,
97+
onlyComments,
7798
});
99+
traverse(ast, visitor);
78100
}

packages/evasive-transform/src/transform-code.js

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* @import {BinaryExpression, Expression, Node, TemplateElement, TemplateLiteral} from '@babel/types'
3+
* @import {NodePath} from '@babel/traverse'
4+
*/
5+
16
const evadeRegexp = /import\s*\(|<!--|-->/g;
27
// The replacement collection for regexp patterns matching the evadeRegexp is only applied to the first matched character, so it is necessary for the regexpReplacements to be maintained together with the evadeRegexp.
38
const regexpReplacements = {
@@ -11,8 +16,8 @@ const regexpReplacements = {
1116
* to sever references), updating the target's end position as if it had zero
1217
* length.
1318
*
14-
* @param {import('@babel/types').Node} target
15-
* @param {import('@babel/types').Node} src
19+
* @param {Node} target
20+
* @param {Node} src
1621
*/
1722
const adoptStartFrom = (target, src) => {
1823
try {
@@ -37,9 +42,9 @@ const adoptStartFrom = (target, src) => {
3742
/**
3843
* Creates a BinaryExpression adding two expressions
3944
*
40-
* @param {import('@babel/types').Expression} left
45+
* @param {Expression} left
4146
* @param {string} rightString
42-
* @returns {import('@babel/types').BinaryExpression}
47+
* @returns {BinaryExpression}
4348
*/
4449
const addStringToExpressions = (left, rightString) => ({
4550
type: 'BinaryExpression',
@@ -55,15 +60,15 @@ const addStringToExpressions = (left, rightString) => ({
5560
* Break up problematic substrings into concatenation expressions, e.g.
5661
* `"import("` -> `"im"+"port("`.
5762
*
58-
* @param {import('@babel/traverse').NodePath} p
63+
* @param {NodePath} p
5964
*/
6065
export const evadeStrings = p => {
6166
const { node } = p;
6267
if (node.type !== 'StringLiteral') {
6368
return;
6469
}
6570
const { value } = node;
66-
/** @type {import('@babel/types').Expression | undefined} */
71+
/** @type {Expression | undefined} */
6772
let expr;
6873
let lastIndex = 0;
6974
for (const match of value.matchAll(evadeRegexp)) {
@@ -85,10 +90,9 @@ export const evadeStrings = p => {
8590
* Break up problematic substrings in template literals with empty-string
8691
* expressions, e.g. `import(` -> `im${''}port(`.
8792
*
88-
* @param {import('@babel/traverse').NodePath} p
93+
* @param {NodePath} p
8994
*/
9095
export const evadeTemplates = p => {
91-
/** @type {import('@babel/types').TemplateLiteral} */
9296
const node = p.node;
9397
// The transform is only meaning-preserving if not part of a
9498
// TaggedTemplateExpression, so these need to be excluded until a motivating
@@ -107,11 +111,14 @@ export const evadeTemplates = p => {
107111
// Check if any quasi needs transformation
108112
if (!quasis.some(quasi => quasi.value.raw.search(evadeRegexp) !== -1)) return;
109113

110-
/** @type {import('@babel/types').TemplateElement[]} */
114+
/** @type {TemplateElement[]} */
111115
const newQuasis = [];
112-
/** @type {import('@babel/types').Expression[]} */
116+
/** @type {Expression[]} */
113117
const newExpressions = [];
114118

119+
/**
120+
* @param {string} quasiValue
121+
*/
115122
const addQuasi = quasiValue => {
116123
// Insert empty expression to break the pattern
117124
newExpressions.push({
@@ -130,6 +137,7 @@ export const evadeTemplates = p => {
130137
});
131138
};
132139

140+
// eslint-disable-next-line @endo/restrict-comparison-operands
133141
for (let i = 0; i < quasis.length; i += 1) {
134142
const quasi = quasis[i];
135143
// We're not currently preserving raw vs. cooked literal data.
@@ -158,6 +166,7 @@ export const evadeTemplates = p => {
158166
}
159167

160168
// Add original expression between quasis
169+
// eslint-disable-next-line @endo/restrict-comparison-operands
161170
if (i < node.expressions.length) {
162171
// @ts-ignore whatever was there, must still be allowed.
163172
newExpressions.push(node.expressions[i]);
@@ -169,7 +178,7 @@ export const evadeTemplates = p => {
169178
newQuasis[newQuasis.length - 1].tail = true;
170179
}
171180

172-
/** @type {import('@babel/types').Node} */
181+
/** @type {Node} */
173182
const replacement = {
174183
type: 'TemplateLiteral',
175184
quasis: newQuasis,
@@ -185,7 +194,7 @@ export const evadeTemplates = p => {
185194
*
186195
* `/import(/` -> `/im[p]ort(/`
187196
*
188-
* @param {import('@babel/traverse').NodePath} p
197+
* @param {NodePath} p
189198
* @returns {void}
190199
*/
191200
export const evadeRegexpLiteral = p => {
@@ -207,7 +216,7 @@ export const evadeRegexpLiteral = p => {
207216
* Prevents `-->` from appearing in output by transforming
208217
* `x-->y` to `(0,x--)>y`.
209218
*
210-
* @param {import('@babel/traverse').NodePath} p
219+
* @param {NodePath} p
211220
* @returns {void}
212221
*/
213222
export const evadeDecrementGreater = p => {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Types for `createEvasiveTransformPass`.
3+
*
4+
* @module
5+
*/
6+
7+
import type { Visitor } from '@babel/traverse';
8+
9+
/**
10+
* A mutating transform pass that rewrites AST nodes in place.
11+
*
12+
* Matches the `TransformPass` interface from `@endo/parser-pipeline`.
13+
*/
14+
export interface TransformPass {
15+
readonly visitor: Visitor;
16+
}
17+
18+
/**
19+
* Options for {@link createEvasiveTransformPass}.
20+
*/
21+
export interface CreateEvasiveTransformPassOptions {
22+
/**
23+
* Empties comments but preserves interior newlines.
24+
*/
25+
elideComments?: boolean;
26+
27+
/**
28+
* If true, limits transformation to comment contents, preserving code
29+
* positions within each line.
30+
*/
31+
onlyComments?: boolean;
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Provides {@link createEvasiveTransformPass}, a {@link TransformPass} adapter
3+
* for evasive transforms.
4+
*
5+
* @module
6+
*/
7+
8+
/**
9+
* @import {TransformPass, CreateEvasiveTransformPassOptions} from './transform-pass-types.js'
10+
*/
11+
12+
import { evadeComment, elideComment } from './transform-comment.js';
13+
import { makeTransformCommentsVisitor } from './transform-ast.js';
14+
15+
/**
16+
* Creates a {@link TransformPass} compatible with `@endo/parser-pipeline` that
17+
* applies SES censorship evasion transforms as AST mutations.
18+
*
19+
* The visitor applies the same transforms as {@link evadeCensorSync} but
20+
* operates on an existing AST rather than parsing from source.
21+
*
22+
* @param {CreateEvasiveTransformPassOptions} [options]
23+
* @returns {TransformPass}
24+
*/
25+
export const createEvasiveTransformPass = (options = {}) => {
26+
const { elideComments = false, onlyComments = false } = options;
27+
const transformComment = elideComments ? elideComment : evadeComment;
28+
29+
return {
30+
visitor: makeTransformCommentsVisitor({ transformComment, onlyComments }),
31+
};
32+
};

0 commit comments

Comments
 (0)