Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/itchy-meteors-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@endo/evasive-transform': minor
---

Expose `makeTransformCommentsVisitor()` via the `visitor.js` entry point for use with `@endo/parser-pipeline`.
5 changes: 4 additions & 1 deletion packages/evasive-transform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"module": "./index.js",
"exports": {
".": "./index.js",
"./visitor.js": "./src/visitor.js",
"./package.json": "./package.json"
},
"scripts": {
Expand All @@ -40,6 +41,7 @@
"devDependencies": {
"@babel/types": "~7.28.2",
"@endo/ses-ava": "workspace:^",
"@types/babel__generator": "~7.27.0",
"ava": "catalog:dev",
"c8": "catalog:dev",
"eslint": "catalog:dev",
Expand Down Expand Up @@ -74,6 +76,7 @@
"dependencies": {
"@babel/generator": "^7.28.3",
"@babel/parser": "~7.28.3",
"@babel/traverse": "~7.28.3"
"@babel/traverse": "~7.28.3",
"@types/babel__traverse": "~7.28.0"
}
}
11 changes: 4 additions & 7 deletions packages/evasive-transform/src/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,13 @@ const generator = /** @type {typeof import('@babel/generator')['default']} */ (
babelGenerator.default || babelGenerator
);

/**
* @typedef {NonNullable<import('@babel/generator').GeneratorOptions['inputSourceMap']>} SourceMapOption
*/

/**
* Options for {@link generateCode} with source map
*
* @typedef GenerateAstOptionsWithSourceMap
* @property {string} [source]
* @property {string} sourceUrl - If present, we will generate a source map
* @property {SourceMapOption | undefined} [sourceMap] - If present, the generated source map will be a transform over the given source map.
* @property {object | string | undefined} [sourceMap] - If present, the generated source map will be a transform over the given source map.
* @internal
*/

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

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

/**
* Generates new code from a Babel AST; returns code only
*@overload
* @overload
* @param {import('@babel/types').File} ast - Babel "File" AST
* @param {GenerateAstOptions} [options] - Options for the transform
* @returns {TransformedResult}
Expand All @@ -93,6 +89,7 @@ export const generate = (ast, options) => {
{
sourceFileName: sourceUrl,
sourceMaps: Boolean(sourceUrl),
// @ts-expect-error undocumented option
inputSourceMap,
retainLines: true,
...(source === undefined ? {} : { experimental_preserveFormat: true }),
Expand Down
5 changes: 2 additions & 3 deletions packages/evasive-transform/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

/**
* @import {TransformedResult, TransformedResultWithSourceMap} from './generate.js'
* @import {SourceMapOption} from './generate.js'
*/

import { transformAst } from './transform-ast.js';
Expand All @@ -17,13 +16,13 @@ import { generate } from './generate.js';
* Options for {@link evadeCensorSync}
*
* @typedef EvadeCensorOptions
* @property {SourceMapOption | undefined} [sourceMap] - Original source map in JSON string or object form
* @property {object | string | undefined} [sourceMap] - Original source map in JSON string or object form
* @property {string | undefined} [sourceUrl] - URL or filepath of the original source in `code`
* @property {boolean | undefined} [elideComments] - Empties the comments but preserves interior newlines.
* @property {import('./parse-ast.js').SourceType | undefined} [sourceType] - Module source type
* @property {boolean | undefined} [onlyComments] - if true, will limit transformation to
comment contents, preserving code positions within each line
* @property {(path: import('@babel/traverse').NodePath) => void} [customVisitor] - A visitor function to be called on each node, in addition to the standard transforms. Receives the same path argument as a normal Babel visitor.
* @property {(path: import('@babel/traverse').NodePath) => void} [customVisitor] - A visitor function to be called on each node, in addition to the standard transforms. Receives the same path argument as a normal Babel visitor.
* @property {boolean | undefined} [useLocationUnmap] - deprecated, vestigial
* @public
*/
Expand Down
5 changes: 3 additions & 2 deletions packages/evasive-transform/src/parse-ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { parse: parseBabel } = babelParser;
* This is a subset of `@babel/parser`'s `ParserOptions['sourceType']`, but
* re-implemented here for decoupling purposes.
*
* @typedef {'module' | 'script'} SourceType
* @typedef {'module' | 'script' | 'commonjs'} SourceType
* @public
*/

Expand All @@ -37,7 +37,8 @@ export function parseAst(source, opts = {}) {
return parseBabel(source, {
tokens: true,
createParenthesizedExpressions: true,
allowReturnOutsideFunction: opts.sourceType === 'script',
allowReturnOutsideFunction:
opts.sourceType === 'script' || opts.sourceType === 'commonjs',
...(opts.sourceType !== undefined && { sourceType: opts.sourceType }),
});
}
66 changes: 40 additions & 26 deletions packages/evasive-transform/src/transform-ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import {
evadeMethod,
} from './transform-code.js';

/**
* @import {Visitor, NodePath} from '@babel/traverse'
* @import {File} from '@babel/types'
*/

// TODO The following is sufficient on Node.js, but for compatibility with
// `node -r esm`, we must use the pattern below.
// Restore after https://github.com/Agoric/agoric-sdk/issues/8671.
Expand All @@ -24,48 +29,35 @@ const traverse = /** @type {typeof import('@babel/traverse')['default']} */ (
);

/**
* Options for {@link transformAst}
* Options shared by {@link makeEvasiveTransformVisitor} and {@link transformAst}.
*
* @internal
* @typedef {TransformAstOptionsWithoutSourceMap} TransformAstOptions
*/

/**
* Options for {@link transformAst}
*
* @internal
* @typedef TransformAstOptionsWithoutSourceMap
* @typedef TransformAstOptions
* @property {boolean} [elideComments]
* @property {boolean} [onlyComments]
* @property {(path: import('@babel/traverse').NodePath) => void} [customVisitor] - A visitor function to be called on each node, in addition to the standard transforms. Receives the same path argument as a normal Babel visitor.
* @property {(path: NodePath) => void} [customVisitor] - A visitor function to be called on each node, in addition to the standard transforms. Receives the same path argument as a normal Babel visitor.
*/

/**
* Performs transformations on the given AST
*
* This function mutates `ast`.
* Factory for a Babel {@link Visitor} which performs evasive transformations on the AST.
*
* @internal
* @param {import('@babel/types').File} ast - AST, as generated by Babel
* @param {TransformAstOptions} [opts]
* @returns {void}
* @param {TransformAstOptions} options
* @returns {Visitor}
*/
export function transformAst(
ast,
{ elideComments = false, onlyComments = false, customVisitor } = {},
) {
export const makeEvasiveTransformVisitor = ({
elideComments = false,
onlyComments = false,
customVisitor,
} = {}) => {
const transformComment = elideComments ? elideComment : evadeComment;
traverse(ast, {
return {
enter(p) {
const { leadingComments, innerComments, trailingComments, type } = p.node;
// discriminated union
if ('comments' in p.node) {
(p.node.comments || []).forEach(node => transformComment(node));
}
// Rewrite all comments.
(leadingComments || []).forEach(node => transformComment(node));
if (type.startsWith('Comment')) {
transformComment(p.node);
transformComment(/** @type {any} */ (p.node));
}
(innerComments || []).forEach(node => transformComment(node));
(trailingComments || []).forEach(node => transformComment(node));
Expand All @@ -78,5 +70,27 @@ export function transformAst(
customVisitor?.(p);
}
},
};
};

/**
* Performs transformations on the given AST
*
* This function mutates `ast`.
*
* @internal
* @param {File} ast - AST, as generated by Babel
* @param {TransformAstOptions} [opts]
* @returns {void}
*/
export function transformAst(
ast,
{ elideComments = false, onlyComments = false, customVisitor } = {},
) {
const visitor = makeEvasiveTransformVisitor({
elideComments,
onlyComments,
customVisitor,
});
traverse(ast, visitor);
}
47 changes: 28 additions & 19 deletions packages/evasive-transform/src/transform-code.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* @import {BinaryExpression, Expression, Node, TemplateElement} from '@babel/types'
* @import {NodePath} from '@babel/traverse'
*/

const evadeRegexp = /import\s*\(|<!--|-->/g;
// 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.
const regexpReplacements = {
Expand All @@ -11,8 +16,8 @@ const regexpReplacements = {
* to sever references), updating the target's end position as if it had zero
* length.
*
* @param {import('@babel/types').Node} target
* @param {import('@babel/types').Node} src
* @param {Node} target
* @param {Node} src
*/
const adoptStartFrom = (target, src) => {
try {
Expand All @@ -37,9 +42,9 @@ const adoptStartFrom = (target, src) => {
/**
* Creates a BinaryExpression adding two expressions
*
* @param {import('@babel/types').Expression} left
* @param {Expression} left
* @param {string} rightString
* @returns {import('@babel/types').BinaryExpression}
* @returns {BinaryExpression}
*/
const addStringToExpressions = (left, rightString) => ({
type: 'BinaryExpression',
Expand All @@ -55,15 +60,15 @@ const addStringToExpressions = (left, rightString) => ({
* Break up problematic substrings into concatenation expressions, e.g.
* `"import("` -> `"im"+"port("`.
*
* @param {import('@babel/traverse').NodePath} p
* @param {NodePath} p
*/
export const evadeStrings = p => {
const { node } = p;
if (node.type !== 'StringLiteral') {
return;
}
const { value } = node;
/** @type {import('@babel/types').Expression | undefined} */
/** @type {Expression | undefined} */
let expr;
let lastIndex = 0;
for (const match of value.matchAll(evadeRegexp)) {
Expand All @@ -85,10 +90,9 @@ export const evadeStrings = p => {
* Break up problematic substrings in template literals with empty-string
* expressions, e.g. `import(` -> `im${''}port(`.
*
* @param {import('@babel/traverse').NodePath} p
* @param {NodePath} p
*/
export const evadeTemplates = p => {
/** @type {import('@babel/types').TemplateLiteral} */
const node = p.node;
// The transform is only meaning-preserving if not part of a
// TaggedTemplateExpression, so these need to be excluded until a motivating
Expand All @@ -107,11 +111,14 @@ export const evadeTemplates = p => {
// Check if any quasi needs transformation
if (!quasis.some(quasi => quasi.value.raw.search(evadeRegexp) !== -1)) return;

/** @type {import('@babel/types').TemplateElement[]} */
/** @type {TemplateElement[]} */
const newQuasis = [];
/** @type {import('@babel/types').Expression[]} */
/** @type {Expression[]} */
const newExpressions = [];

/**
* @param {string} quasiValue
*/
const addQuasi = quasiValue => {
// Insert empty expression to break the pattern
newExpressions.push({
Expand All @@ -130,6 +137,7 @@ export const evadeTemplates = p => {
});
};

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

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

/** @type {import('@babel/types').Node} */
/** @type {Node} */
const replacement = {
type: 'TemplateLiteral',
quasis: newQuasis,
Expand All @@ -185,7 +194,7 @@ export const evadeTemplates = p => {
*
* `/import(/` -> `/im[p]ort(/`
*
* @param {import('@babel/traverse').NodePath} p
* @param {NodePath} p
* @returns {void}
*/
export const evadeRegexpLiteral = p => {
Expand All @@ -207,7 +216,7 @@ export const evadeRegexpLiteral = p => {
* Prevents `-->` from appearing in output by transforming
* `x-->y` to `(0,x--)>y`.
*
* @param {import('@babel/traverse').NodePath} p
* @param {NodePath} p
* @returns {void}
*/
export const evadeDecrementGreater = p => {
Expand All @@ -228,20 +237,20 @@ export const evadeDecrementGreater = p => {
};

const EVADE_METHODS = ['import', 'eval'];

/**
* @param {import('@babel/traverse').NodePath} p
* @param {NodePath} p
*/
export const evadeMethod = p => {
const { node } = p;
// find class and object definitions with a method name we need to evade.
// E.g. import() -> `['import']()`
const isMethod = p.isObjectMethod() || p.isClassMethod();
if (
isMethod &&
node.key.type === 'Identifier' &&
EVADE_METHODS.includes(node.key.name)
p.node.key.type === 'Identifier' &&
EVADE_METHODS.includes(p.node.key.name)
) {
node.computed = true;
node.key = { type: 'StringLiteral', value: node.key.name };
p.node.computed = true;
p.node.key = { type: 'StringLiteral', value: p.node.key.name };
}
};
12 changes: 12 additions & 0 deletions packages/evasive-transform/src/visitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Low-level Babel visitor factory for use with `@endo/parser-pipeline`.
*
* Exports {@link makeEvasiveTransformVisitor} to build a
* pipeline-compatible `TransformPass` from the evasive-transform logic.
* The actual `createEvasiveTransformPass` wrapper lives in
* `@endo/parser-pipeline`.
*
* @module
*/

export { makeEvasiveTransformVisitor } from './transform-ast.js';
3 changes: 2 additions & 1 deletion packages/evasive-transform/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"extends": "../../tsconfig.eslint-base.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"stripInternal": true
},
"include": [
"*.js",
Expand Down
1 change: 1 addition & 0 deletions packages/evasive-transform/visitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { makeEvasiveTransformVisitor } from './src/visitor.js';
Loading
Loading