Skip to content

Commit

Permalink
feat(evasive-transform): elideComments option
Browse files Browse the repository at this point in the history
  • Loading branch information
kriskowal committed Aug 20, 2024
1 parent 46ca503 commit 0371e25
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 19 deletions.
6 changes: 6 additions & 0 deletions packages/evasive-transform/NEWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
User-visible changes in `@endo/evasive-transform`:

# Next release

- Adds an `elideComments` option to replace the interior of comments with
minimal blank space with identical cursor advancement behavior.
17 changes: 14 additions & 3 deletions packages/evasive-transform/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { generate } from './generate.js';
* @property {string|import('source-map').RawSourceMap} [sourceMap] - Original source map in JSON string or object form
* @property {string} [sourceUrl] - URL or filepath of the original source in `code`
* @property {boolean} [useLocationUnmap] - Enable location unmapping. Only applies if `sourceMap` was provided
* @property {boolean} [elideComments] - Replace comments with an ellipsis but preserve interior newlines.
* @property {import('./parse-ast.js').SourceType} [sourceType] - Module source type
* @public
*/
Expand All @@ -41,7 +42,13 @@ import { generate } from './generate.js';
export function evadeCensorSync(source, options) {
// TODO Use options ?? {} when resolved:
// https://github.com/Agoric/agoric-sdk/issues/8671
const { sourceMap, sourceUrl, useLocationUnmap, sourceType } = options || {};
const {
sourceMap,
sourceUrl,
useLocationUnmap,
sourceType,
elideComments = false,
} = options || {};

// Parse the rolled-up chunk with Babel.
// We are prepared for different module systems.
Expand All @@ -53,9 +60,13 @@ export function evadeCensorSync(source, options) {
typeof sourceMap === 'string' ? sourceMap : JSON.stringify(sourceMap);

if (sourceMap && useLocationUnmap) {
transformAst(ast, { sourceMap: sourceMapJson, useLocationUnmap });
transformAst(ast, {
sourceMap: sourceMapJson,
useLocationUnmap,
elideComments,
});
} else {
transformAst(ast);
transformAst(ast, { elideComments });
}

if (sourceUrl) {
Expand Down
10 changes: 8 additions & 2 deletions packages/evasive-transform/src/transform-ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import babelTraverse from '@babel/traverse';
import { transformComment } from './transform-comment.js';
import { evadeComment, elideComment } from './transform-comment.js';
import { makeLocationUnmapper } from './location-unmapper.js';

// TODO The following is sufficient on Node.js, but for compatibility with
Expand Down Expand Up @@ -34,6 +34,7 @@ const traverse = /** @type {typeof import('@babel/traverse')['default']} */ (
* @typedef TransformAstOptionsWithoutSourceMap
* @property {false} [useLocationUnmap] - Enable location unmapping
* @property {string} [sourceMap] - Original source map
* @property {boolean} [elideComments]
*/

/**
Expand All @@ -45,6 +46,7 @@ const traverse = /** @type {typeof import('@babel/traverse')['default']} */ (
* @typedef TransformAstOptionsWithLocationUnmap
* @property {true} useLocationUnmap - Enable location unmapping
* @property {string} sourceMap - Original source map
* @property {boolean} [elideComments]
*/

/**
Expand All @@ -57,12 +59,16 @@ const traverse = /** @type {typeof import('@babel/traverse')['default']} */ (
* @param {TransformAstOptions} [opts]
* @returns {void}
*/
export function transformAst(ast, { sourceMap, useLocationUnmap } = {}) {
export function transformAst(
ast,
{ sourceMap, useLocationUnmap, elideComments = false } = {},
) {
/** @type {import('./location-unmapper.js').LocationUnmapper|undefined} */
let unmapLoc;
if (sourceMap && useLocationUnmap) {
unmapLoc = makeLocationUnmapper(sourceMap, ast);
}
const transformComment = elideComments ? elideComment : evadeComment;
traverse(ast, {
enter(p) {
const { loc, leadingComments, innerComments, trailingComments, type } =
Expand Down
47 changes: 44 additions & 3 deletions packages/evasive-transform/src/transform-comment.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Provides {@link transformComment} which evades SES restrictions by modifying
* a Babel AST Node.
* Provides {@link evadeComment} and {@link elideComment} which evade SES
* restrictions by modifying a Babel AST Node.
*
* @module
*/
Expand Down Expand Up @@ -28,7 +28,7 @@ const HTML_COMMENT_END_RE = new RegExp(`--${'>'}`, 'g');
* @param {import('@babel/types').Comment} node
* @param {import('./location-unmapper.js').LocationUnmapper} [unmapLoc]
*/
export function transformComment(node, unmapLoc) {
export function evadeComment(node, unmapLoc) {
node.type = 'CommentBlock';
// Within comments...
node.value = node.value
Expand All @@ -46,3 +46,44 @@ export function transformComment(node, unmapLoc) {
unmapLoc(node.loc);
}
}

/**
* Inspects a comment for a hint that it must be preserved by a transform.
*
* @param {string} comment
*/
const markedForPreservation = comment => {
if (comment.startsWith('!')) {
return true;
}
if (comment.startsWith('*')) {
// Detect jsdoc style @preserve, @copyright, @license, @cc_on (IE
// cconditional comments)
return /(?:^|\n)\s*\*?\s*@(?:preserve|copyright|license|cc_on)\b/.test(
comment,
);
}
return false;
};

/**
* Elides all non-newlines before the last line and replaces all non-newlines
* with spaces on the last line.
* This can greatly reduce the size of a well-commented artifact without
* displacing lines or columns in the transformed code.
*
* @param {import('@babel/types').Comment} node
* @param {import('./location-unmapper.js').LocationUnmapper} [unmapLoc]
*/
export const elideComment = (node, unmapLoc) => {
if (node.type === 'CommentBlock') {
if (!markedForPreservation(node.value)) {
node.value = node.value.replace(/[^\n]+\n/g, '\n').replace(/[^\n]/g, ' ');
}
} else {
node.value = '';
}
if (unmapLoc) {
unmapLoc(node.loc);
}
};
168 changes: 168 additions & 0 deletions packages/evasive-transform/test/elide-comment.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { test } from './prepare-test-env-ava-fixture.js';
import { elideComment } from '../src/transform-comment.js';
import { evadeCensorSync } from '../src/index.js';

test('elideComment preserves the column width of the last and only line of a block comment', t => {
const comment = /** @type {import('@babel/types').Comment} */ ({
type: 'CommentBlock',
value: ' hello world ',
});
elideComment(comment);
t.is(comment.value, ' ');
});

test('elideComment erases non-final lines but preserves all newlines in a block comment', t => {
const comment = /** @type {import('@babel/types').Comment} */ ({
type: 'CommentBlock',
value: ' * some\n * unnecessary information \n * the end',
});
elideComment(comment);
t.is(comment.value, '\n\n ');
});

test('elideComment unconditionally elides line comments', t => {
const comment = /** @type {import('@babel/types').Comment} */ ({
type: 'CommentLine',
value: ' hello',
});
elideComment(comment);
t.is(comment.value, '');
});

test('evadeCensor with elideComments erases the interior of block comments', t => {
const object = evadeCensorSync(
`/**
* Comment
* @param {type} name
*/`,
{ elideComments: true },
);
t.is(
object.code,
`/*
*/`,
);
});

test('evadeCensor with elideComments elides line comments', t => {
const object = evadeCensorSync(`// hello`, { elideComments: true });
t.is(object.code, `//`);
});

test('evadeCensor with elideComments preserves bang comments', t => {
const object = evadeCensorSync(`/*! kris wuz here */`, {
elideComments: true,
});
t.is(object.code, `/*! kris wuz here */`);
});

test('evadeCensor with elideComments preserves jsdoc @preserve comments', t => {
const comment = `/**
* @preserve
*/`;
const object = evadeCensorSync(comment, {
elideComments: true,
});
t.is(object.code, comment);
});

test('evadeCensor with elideComments preserves initial jsdoc @preserve comments', t => {
const comment = `/** @preserve
*/`;
const object = evadeCensorSync(comment, {
elideComments: true,
});
t.is(object.code, comment);
});

test('evadeCensor with elideComments preserves artless-but-valid jsdoc @preserve comments', t => {
const comment = `/**
@preserve
*/`;
const object = evadeCensorSync(comment, {
elideComments: true,
});
t.is(object.code, comment);
});

test('evadeCensor with elideComments preserves jsdoc @copyright comments', t => {
const comment = `/**
* @copyright
*/`;
const object = evadeCensorSync(comment, {
elideComments: true,
});
t.is(object.code, comment);
});

test('evadeCensor with elideComments preserves jsdoc @license comments', t => {
const comment = `/**
* @license
*/`;
const object = evadeCensorSync(comment, {
elideComments: true,
});
t.is(object.code, comment);
});

test('evadeCensor with elideComments preserves jsdoc @cc_on comments', t => {
const comment = `/**
* @cc_on
*/`;
const object = evadeCensorSync(comment, {
elideComments: true,
});
t.is(object.code, comment);
});

test('evadeCensor with elideComments does not preserve jsdoc @copyrighteous comments', t => {
const comment = `/**
* @copyrighteous
*/`;
const object = evadeCensorSync(comment, {
elideComments: true,
});
t.is(
object.code,
`/*
*/`,
);
});

/* eslint-disable no-eval */

test('evadeCensor with elideComments preserves automatically-inserted-semicolon (ASI)', t => {
const comment = `
(() => {
return /*
*/ 42;
})();
`;
const object = evadeCensorSync(comment, {
elideComments: true,
});
t.is((0, eval)(comment), undefined);
t.is((0, eval)(object.code), undefined);
});

test('evadeCensor with stripComments preserves automatically-inserted-semicolon (ASI)', t => {
t.log(
'There is no stripComments option. This is a trip-fall in case this is attempted.',
);
const comment = `
(() => {
return /*
*/ 42;
})();
`;
const object = evadeCensorSync(comment, {
stripComments: true,
});
t.is((0, eval)(comment), undefined);
t.is((0, eval)(object.code), undefined);
});

/* eslint-enable no-eval */
22 changes: 11 additions & 11 deletions packages/evasive-transform/test/transform-comment.test.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,51 @@
import { test } from './prepare-test-env-ava-fixture.js';
import { transformComment } from '../src/transform-comment.js';
import { evadeComment } from '../src/transform-comment.js';

test('transformComment() - Node type becomes CommentBlock', async t => {
test('evadeComment() - Node type becomes CommentBlock', async t => {
const comment = /** @type {import('@babel/types').Comment} */ ({
value: 'hello world',
});
transformComment(comment);
evadeComment(comment);
t.is(comment.type, 'CommentBlock');
});

test('transformComment() - strip extraneous leading whitespace', async t => {
test('evadeComment() - strip extraneous leading whitespace', async t => {
const comment = /** @type {import('@babel/types').Comment} */ ({
type: 'CommentBlock',
value: ' hello world ',
});
transformComment(comment);
evadeComment(comment);
t.is(comment.value, ' hello world ');
});

test('transformComment() - defang HTML comment', async t => {
test('evadeComment() - defang HTML comment', async t => {
const comment = /** @type {import('@babel/types').Comment} */ ({
type: 'CommentBlock',
value: '<!-- evil code -->',
});
transformComment(comment);
evadeComment(comment);
t.is(comment.value, '<!=- evil code -=>');
});

test('transformComment() - rewrite suspicious import(...)', async t => {
test('evadeComment() - rewrite suspicious import(...)', async t => {
const comment = /** @type {import('@babel/types').Comment} */ ({
type: 'CommentBlock',
value: `/**
* @type {import('c:\\My Documents\\user.js')}
*/`,
});
transformComment(comment);
evadeComment(comment);
t.regex(
comment.value,
new RegExp("\\* @type \\{IMPORT\\('c:\\\\My Documents\\\\user\\.js'\\)"),
);
});

test('transformComment() - rewrite end-of-comment marker', async t => {
test('evadeComment() - rewrite end-of-comment marker', async t => {
const comment = /** @type {import('@babel/types').Comment} */ ({
type: 'CommentBlock',
value: '/** I like turtles */',
});
transformComment(comment);
evadeComment(comment);
t.is(comment.value, '/** I like turtles *X/');
});

0 comments on commit 0371e25

Please sign in to comment.