diff --git a/.commitlintrc.ts b/.commitlintrc.ts
index 738df04..6ca502f 100644
--- a/.commitlintrc.ts
+++ b/.commitlintrc.ts
@@ -21,6 +21,7 @@ const config: UserConfig = {
'scope-enum': [Severity.Error, 'always', scopes([
'chore',
'handlers',
+ 'util',
'visitors'
])]
}
diff --git a/.dictionary.txt b/.dictionary.txt
index 7285309..3088e9b 100644
--- a/.dictionary.txt
+++ b/.dictionary.txt
@@ -32,5 +32,6 @@ shfmt
unassert
unstub
vates
+vfile
vitest
yarnrc
diff --git a/.eslintignore b/.eslintignore
index df0fe1b..c28ac75 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -16,6 +16,7 @@
**/node_modules/
**/tsconfig*temp.json
Brewfile
+__fixtures__/modules/*.cjs
yarn.lock
# NEGATED PATTERNS
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index d63cfe3..ec50ce1 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -10,7 +10,27 @@
*/
const config = {
extends: ['./.eslintrc.base.cjs'],
- overrides: [...require('./.eslintrc.base.cjs').overrides],
+ overrides: [
+ ...require('./.eslintrc.base.cjs').overrides,
+ {
+ files: ['__fixtures__/modules/*.mjs'],
+ rules: {
+ '@typescript-eslint/no-confusing-void-expression': 0,
+ '@typescript-eslint/no-unsafe-argument': 0,
+ '@typescript-eslint/no-unsafe-call': 0,
+ '@typescript-eslint/no-unsafe-member-access': 0,
+ '@typescript-eslint/no-unsafe-return': 0,
+ '@typescript-eslint/require-await': 0,
+ '@typescript-eslint/restrict-plus-operands': 0,
+ '@typescript-eslint/restrict-template-expressions': 0,
+ '@typescript-eslint/strict-boolean-expressions': 0,
+ 'jsdoc/require-file-overview': 0,
+ 'jsdoc/require-jsdoc': 0,
+ 'unicorn/prefer-math-trunc': 0,
+ 'unicorn/prefer-node-protocol': 0
+ }
+ }
+ ],
root: true
}
diff --git a/.lintstagedrc.json b/.lintstagedrc.json
index c42c61f..44ada3c 100644
--- a/.lintstagedrc.json
+++ b/.lintstagedrc.json
@@ -9,6 +9,6 @@
"src/**/*.ts": [
"vitest run --changed --coverage",
"yarn build",
- "bash -c tsc -p tsconfig.build.json"
+ "bash -c tsc -p tsconfig.json"
]
}
diff --git a/__fixtures__/modules/add.mjs b/__fixtures__/modules/add.mjs
new file mode 100644
index 0000000..7d63e38
--- /dev/null
+++ b/__fixtures__/modules/add.mjs
@@ -0,0 +1,18 @@
+import { deprecate, equal, ok } from 'devlop'
+
+function add(a, b) {
+ ok(typeof a === 'number', 'expected `a` to be a number')
+ ok(typeof b === 'number', 'expected `b` to be a number')
+
+ if (process.env.NODE_ENV !== 'production') {
+ equal(Number.isNaN(a), false, 'expected `a` not to equal NaN')
+ equal(Number.isNaN(b), false, 'expected `b` not to equal NaN')
+ } else {
+ ok(!Number.isNaN(a), 'expected `a` not to be NaN')
+ ok(!Number.isNaN(b), 'expected `b` not to be NaN')
+ }
+
+ return a + b
+}
+
+export default deprecate(add)
diff --git a/__fixtures__/modules/assert.mjs b/__fixtures__/modules/assert.mjs
new file mode 100644
index 0000000..bedaf20
--- /dev/null
+++ b/__fixtures__/modules/assert.mjs
@@ -0,0 +1,5 @@
+function assert(value, message, ...params) {
+ return void console.assert(value, message, ...params)
+}
+
+export default assert
diff --git a/__fixtures__/modules/call-expression.mjs b/__fixtures__/modules/call-expression.mjs
new file mode 100644
index 0000000..ca32856
--- /dev/null
+++ b/__fixtures__/modules/call-expression.mjs
@@ -0,0 +1,97 @@
+import {
+ DOT,
+ at,
+ define,
+ set
+} from '@flex-development/tutils'
+import { ok, unreachable } from 'devlop'
+import { CONTINUE } from 'estree-util-visit'
+import { is } from 'unist-util-is'
+
+function CallExpression(node, key, index) {
+ if (
+ (
+ is(node.callee, 'Identifier') &&
+ this.identifiers.has(node.callee.name)
+ ) ||
+ (
+ is(node.callee, 'MemberExpression') &&
+ is(node.callee.object, 'Identifier') &&
+ this.identifiers.has(node.callee.object.name)
+ ) ||
+ (
+ is(node.callee, 'MemberExpression') &&
+ is(node.callee.object, 'Identifier') &&
+ is(node.callee.property, 'Identifier') &&
+ node.callee.object.name === 'console' &&
+ node.callee.property.name === 'assert'
+ )
+ ) {
+ ok(this.parent, 'expected `parent`')
+ ok(key, 'expected `key`')
+ ok(key in this.parent, `expected \`parent.${key}\``)
+ const zero = { raw: '0', type: 'Literal', value: 0 }
+ const void0 = {
+ argument: zero,
+ operator: 'void',
+ prefix: true,
+ type: 'UnaryExpression'
+ }
+ switch (this.parent.type) {
+ case 'ArrayExpression':
+ case 'CallExpression':
+ ok(typeof index === 'number', 'expected `index` to be a number')
+ set(this.parent, key + DOT + index, void0)
+ break
+ case 'AssignmentExpression':
+ if (is(this.grandparent, 'ExpressionStatement')) {
+ this.trash.add(this.grandparent)
+ } else {
+ define(this.parent, key, { value: void0 })
+ }
+ break
+ case 'ArrowFunctionExpression':
+ case 'AssignmentPattern':
+ case 'ConditionalExpression':
+ case 'LogicalExpression':
+ case 'Property':
+ define(this.parent, key, { value: void0 })
+ break
+ case 'AwaitExpression':
+ if (is(this.grandparent, 'ExpressionStatement')) {
+ this.trash.add(this.grandparent)
+ } else {
+ define(this.parent, key, { value: void0 })
+ }
+ break
+ case 'ExpressionStatement':
+ this.trash.add(this.parent)
+ break
+ case 'ExportDefaultDeclaration':
+ define(this.parent, key, { value: at(node.arguments, 0, void0) })
+ break
+ case 'ReturnStatement':
+ case 'YieldExpression':
+ define(this.parent, key, { value: null })
+ break
+ case 'UnaryExpression':
+ if (is(this.grandparent, 'ExpressionStatement')) {
+ this.trash.add(this.grandparent)
+ } else {
+ define(this.parent, key, {
+ value: this.parent.operator === 'void' ? zero : void0
+ })
+ }
+ break
+ default:
+ console.dir(this.parent, { depth: 10 })
+ void unreachable(`unexpected parent: ${this.parent.type}`)
+ }
+ }
+ return CONTINUE
+}
+
+var call_expression_default = CallExpression
+export {
+ call_expression_default as default
+}
diff --git a/__fixtures__/modules/century-from-year.mjs b/__fixtures__/modules/century-from-year.mjs
new file mode 100644
index 0000000..edf90de
--- /dev/null
+++ b/__fixtures__/modules/century-from-year.mjs
@@ -0,0 +1,10 @@
+const { ok } = await import('devlop')
+const assert = await import('node:assert/strict')
+
+const centuryFromYear = year => {
+ ok(typeof year === 'number', 'expected `year` to be a number')
+ assert.notEqual(Number.isNaN(year), true, 'expected `year` not to be NaN')
+ return Math.ceil(year / 100)
+}
+
+export default centuryFromYear
diff --git a/__fixtures__/modules/digitize.mjs b/__fixtures__/modules/digitize.mjs
new file mode 100644
index 0000000..abdaea5
--- /dev/null
+++ b/__fixtures__/modules/digitize.mjs
@@ -0,0 +1,17 @@
+import { ok } from 'devlop'
+import * as assert from 'node:assert/strict'
+
+function digitize(n) {
+ ok(typeof n === 'number', 'expected `n` to be a number')
+
+ if (process.env.NODE_ENV !== 'production') {
+ assert.notEqual(Number.isNaN(n), true, 'expected `n` not to be NaN')
+ } else ok(n >= 0, 'expected `n` to be a non-negative number')
+
+ if (n <= 9) return [n]
+ const digits = []
+ while (n > 0) digits.push(n % 10 | 0) && (n = n / 10 | 0)
+ return digits
+}
+
+export default digitize
diff --git a/__fixtures__/modules/divide.cjs b/__fixtures__/modules/divide.cjs
new file mode 100644
index 0000000..fdd1977
--- /dev/null
+++ b/__fixtures__/modules/divide.cjs
@@ -0,0 +1,13 @@
+'use strict'
+
+let assert = require('assert'), bar = 'BAR', foo = 'FOO'
+
+function divide(a, b) {
+ assert.equal(typeof a, 'number')
+ assert(!isNaN(a))
+ assert.equal(typeof b, 'number')
+ assert.ok(!isNaN(b))
+ return a / b
+}
+
+module.exports = module.exports.default = divide
diff --git a/__fixtures__/modules/gemoji-html.mjs b/__fixtures__/modules/gemoji-html.mjs
new file mode 100644
index 0000000..ce42421
--- /dev/null
+++ b/__fixtures__/modules/gemoji-html.mjs
@@ -0,0 +1,25 @@
+import { ok } from 'devlop'
+import { nameToEmoji } from 'gemoji'
+import { codes } from 'micromark-util-symbol'
+function gemojiHtml() {
+ return {
+ enter: {
+ gemoji() {
+ return void this.tag('')
+ }
+ },
+ exit: {
+ gemoji(token) {
+ const val = this.sliceSerialize(token)
+ ok(val.codePointAt(0) === codes.colon, 'expected `:` start')
+ ok(val.codePointAt(val.length - 1) === codes.colon, 'expected `:` end')
+ this.raw(nameToEmoji[val.slice(1, -1)] ?? val)
+ return void this.tag('')
+ }
+ }
+ }
+}
+var html_default = gemojiHtml
+export {
+ html_default as default
+}
diff --git a/__fixtures__/modules/gemoji-shortcode.mjs b/__fixtures__/modules/gemoji-shortcode.mjs
new file mode 100644
index 0000000..87c712d
--- /dev/null
+++ b/__fixtures__/modules/gemoji-shortcode.mjs
@@ -0,0 +1,49 @@
+import { ok as assert } from 'devlop'
+import { asciiAlphanumeric } from 'micromark-util-character'
+import { codes } from 'micromark-util-symbol'
+function previous(code) {
+ return code !== codes.backslash && code !== codes.colon
+}
+var shortcode_default = {
+ name: 'gemoji',
+ previous,
+ tokenize(effects, ok, nok) {
+ function inside(code) {
+ switch (true) {
+ case code === codes.colon:
+ effects.consume(code)
+ effects.exit('gemoji')
+ return ok
+ case asciiAlphanumeric(code):
+ case code === codes.dash:
+ case code === codes.plusSign:
+ case code === codes.underscore:
+ effects.consume(code)
+ return inside
+ default:
+ return nok(code)
+ }
+ }
+ function begin(code) {
+ switch (code) {
+ case codes.eof:
+ case codes.colon:
+ return nok(code)
+ default:
+ effects.consume(code)
+ return inside
+ }
+ }
+ const start = code => {
+ assert(code === codes.colon, 'expected `:`')
+ assert(previous.call(this, this.previous), 'expected correct previous')
+ effects.enter('gemoji')
+ effects.consume(code)
+ return begin
+ }
+ return start
+ }
+}
+export {
+ shortcode_default as default
+}
diff --git a/__fixtures__/modules/http-assert.cjs b/__fixtures__/modules/http-assert.cjs
new file mode 100644
index 0000000..935588a
--- /dev/null
+++ b/__fixtures__/modules/http-assert.cjs
@@ -0,0 +1,14 @@
+'use strict'
+
+const assert = require('http-assert')
+const { ok, equal: eq, deepEqual: deq } = require('node:assert')
+
+module.exports = module.exports.default = function(username, check) {
+ try {
+ assert(username == check, 401, 'authentication failed')
+ } catch (err) {
+ eq(err.status, 401)
+ deq(err.message, 'authentication failed')
+ ok(err.expose)
+ }
+}
diff --git a/__fixtures__/modules/human-readable.mjs b/__fixtures__/modules/human-readable.mjs
new file mode 100644
index 0000000..e034edb
--- /dev/null
+++ b/__fixtures__/modules/human-readable.mjs
@@ -0,0 +1,22 @@
+import { ok } from 'devlop'
+import * as assert from 'node:assert/strict'
+
+async function humanReadable(seconds) {
+ ok(typeof seconds === 'number', 'expected `seconds` to be a number')
+ await assert.doesNotReject(async () => seconds.toFixed(2))
+
+ return new Promise(resolve => {
+ let formatted = ''
+
+ for (const converter of [3600, 60, 1]) {
+ const time = seconds / converter | 0
+ formatted += time < 10 ? `0${time}` : time
+ if (converter !== 1) formatted += ':'
+ seconds -= time * converter
+ }
+
+ return resolve(formatted)
+ })
+}
+
+export default humanReadable
diff --git a/__fixtures__/modules/multiply.cjs b/__fixtures__/modules/multiply.cjs
new file mode 100644
index 0000000..87e69da
--- /dev/null
+++ b/__fixtures__/modules/multiply.cjs
@@ -0,0 +1,13 @@
+'use strict'
+
+const { strict: assert } = require('assert')
+
+const multiply = function(a, b) {
+ console.assert(typeof a === 'number')
+ assert(!isNaN(a))
+ assert.equal(typeof b, 'number')
+ assert.ok(!isNaN(b))
+ return a * b
+}
+
+module.exports = module.exports.default = multiply
diff --git a/__fixtures__/modules/noop.mjs b/__fixtures__/modules/noop.mjs
new file mode 100644
index 0000000..a28ac7b
--- /dev/null
+++ b/__fixtures__/modules/noop.mjs
@@ -0,0 +1,3 @@
+import { ok } from 'devlop'
+
+export default () => ok(true)
diff --git a/__fixtures__/modules/subtract.mjs b/__fixtures__/modules/subtract.mjs
new file mode 100644
index 0000000..d6413f7
--- /dev/null
+++ b/__fixtures__/modules/subtract.mjs
@@ -0,0 +1,9 @@
+import * as assert from 'node:assert'
+
+function subtract(a, b) {
+ assert.ok(typeof a === 'number', 'expected a to be a number')
+ assert.ok(typeof b === 'number', 'expected b to be a number')
+ return a - b
+}
+
+export default subtract
diff --git a/build.config.ts b/build.config.ts
index 94e40b9..d99a4d3 100644
--- a/build.config.ts
+++ b/build.config.ts
@@ -5,6 +5,10 @@
*/
import { defineBuildConfig, type Config } from '@flex-development/mkbuild'
+import { constant, define } from '@flex-development/tutils'
+import { ok } from 'devlop'
+import type { BuildResult, PluginBuild } from 'esbuild'
+import util from 'node:util'
import pkg from './package.json' assert { type: 'json' }
import tsconfig from './tsconfig.build.json' assert { type: 'json' }
@@ -38,6 +42,46 @@ const config: Config = defineBuildConfig({
sourcesContent: false
}
],
+ plugins: [
+ {
+ /**
+ * Plugin name.
+ */
+ name: 'rollup-pluginutils-specifier',
+
+ /**
+ * Fix the `@rollup/pluginutils` module specifier.
+ *
+ * @this {void}
+ *
+ * @param {PluginBuild} build - esbuild plugin api
+ * @return {void} Nothing
+ */
+ setup(build: PluginBuild): void {
+ /**
+ * Regular expression used to fix module specifier.
+ *
+ * @const {RegExp} regex
+ */
+ const regex: RegExp = /(["'])(@rollup\/pluginutils).+(["'])/
+
+ // fix module specifiers on build end
+ return void build.onEnd((result: BuildResult): void => {
+ ok(result.outputFiles, 'expected output files')
+
+ for (const output of result.outputFiles) {
+ if (/\.[cm]?[jt]s$/.test(output.path)) {
+ let { text } = output
+
+ text = text.replace(regex, '$1$2$1')
+ define(output, 'text', { get: constant(text) })
+ output.contents = new util.TextEncoder().encode(text)
+ }
+ }
+ })
+ }
+ }
+ ],
target: [
pkg.engines.node.replace(/^\D+/, 'node'),
tsconfig.compilerOptions.target
diff --git a/package.json b/package.json
index 633a796..f1ac077 100644
--- a/package.json
+++ b/package.json
@@ -145,9 +145,11 @@
"remark-gfm": "4.0.0",
"sh-syntax": "0.4.2",
"source-map": "0.8.0-beta.0",
+ "to-vfile": "8.0.0",
"trash-cli": "5.0.0",
"ts-dedent": "2.2.0",
"typescript": "5.3.3",
+ "vfile": "6.0.1",
"vite-tsconfig-paths": "4.3.1",
"vitest": "1.3.1",
"yaml-eslint-parser": "1.2.2"
diff --git a/src/__snapshots__/unassert.integration.snap b/src/__snapshots__/unassert.integration.snap
new file mode 100644
index 0000000..9968eb2
--- /dev/null
+++ b/src/__snapshots__/unassert.integration.snap
@@ -0,0 +1,250 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`integration:unassert > remove assertions > program sample 0: add.mjs 1`] = `
+"function add(a, b) {
+ return a + b;
+}
+export default add;
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 1: assert.mjs 1`] = `
+"function assert(value, message, ...params) {
+ return void 0;
+}
+export default assert;
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 2: call-expression.mjs 1`] = `
+"import {DOT, at, define, set} from "@flex-development/tutils";
+import {CONTINUE} from "estree-util-visit";
+import {is} from "unist-util-is";
+function CallExpression(node, key, index) {
+ if (is(node.callee, "Identifier") && this.identifiers.has(node.callee.name) || is(node.callee, "MemberExpression") && is(node.callee.object, "Identifier") && this.identifiers.has(node.callee.object.name) || is(node.callee, "MemberExpression") && is(node.callee.object, "Identifier") && is(node.callee.property, "Identifier") && node.callee.object.name === "console" && node.callee.property.name === "assert") {
+ const zero = {
+ raw: "0",
+ type: "Literal",
+ value: 0
+ };
+ const void0 = {
+ argument: zero,
+ operator: "void",
+ prefix: true,
+ type: "UnaryExpression"
+ };
+ switch (this.parent.type) {
+ case "ArrayExpression":
+ case "CallExpression":
+ set(this.parent, key + DOT + index, void0);
+ break;
+ case "AssignmentExpression":
+ if (is(this.grandparent, "ExpressionStatement")) {
+ this.trash.add(this.grandparent);
+ } else {
+ define(this.parent, key, {
+ value: void0
+ });
+ }
+ break;
+ case "ArrowFunctionExpression":
+ case "AssignmentPattern":
+ case "ConditionalExpression":
+ case "LogicalExpression":
+ case "Property":
+ define(this.parent, key, {
+ value: void0
+ });
+ break;
+ case "AwaitExpression":
+ if (is(this.grandparent, "ExpressionStatement")) {
+ this.trash.add(this.grandparent);
+ } else {
+ define(this.parent, key, {
+ value: void0
+ });
+ }
+ break;
+ case "ExpressionStatement":
+ this.trash.add(this.parent);
+ break;
+ case "ExportDefaultDeclaration":
+ define(this.parent, key, {
+ value: at(node.arguments, 0, void0)
+ });
+ break;
+ case "ReturnStatement":
+ case "YieldExpression":
+ define(this.parent, key, {
+ value: null
+ });
+ break;
+ case "UnaryExpression":
+ if (is(this.grandparent, "ExpressionStatement")) {
+ this.trash.add(this.grandparent);
+ } else {
+ define(this.parent, key, {
+ value: this.parent.operator === "void" ? zero : void0
+ });
+ }
+ break;
+ default:
+ console.dir(this.parent, {
+ depth: 10
+ });
+ }
+ }
+ return CONTINUE;
+}
+var call_expression_default = CallExpression;
+export {call_expression_default as default};
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 3: century-from-year.mjs 1`] = `
+"const centuryFromYear = year => {
+ return Math.ceil(year / 100);
+};
+export default centuryFromYear;
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 4: digitize.mjs 1`] = `
+"function digitize(n) {
+ if (n <= 9) return [n];
+ const digits = [];
+ while (n > 0) digits.push(n % 10 | 0) && (n = n / 10 | 0);
+ return digits;
+}
+export default digitize;
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 5: divide.cjs 1`] = `
+""use strict";
+let bar = "BAR", foo = "FOO";
+function divide(a, b) {
+ return a / b;
+}
+module.exports = module.exports.default = divide;
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 6: gemoji-html.mjs 1`] = `
+"import {nameToEmoji} from "gemoji";
+import {codes} from "micromark-util-symbol";
+function gemojiHtml() {
+ return {
+ enter: {
+ gemoji() {
+ return void this.tag("");
+ }
+ },
+ exit: {
+ gemoji(token) {
+ const val = this.sliceSerialize(token);
+ this.raw(nameToEmoji[val.slice(1, -1)] ?? val);
+ return void this.tag("");
+ }
+ }
+ };
+}
+var html_default = gemojiHtml;
+export {html_default as default};
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 7: gemoji-shortcode.mjs 1`] = `
+"import {asciiAlphanumeric} from "micromark-util-character";
+import {codes} from "micromark-util-symbol";
+function previous(code) {
+ return code !== codes.backslash && code !== codes.colon;
+}
+var shortcode_default = {
+ name: "gemoji",
+ previous,
+ tokenize(effects, ok, nok) {
+ function inside(code) {
+ switch (true) {
+ case code === codes.colon:
+ effects.consume(code);
+ effects.exit("gemoji");
+ return ok;
+ case asciiAlphanumeric(code):
+ case code === codes.dash:
+ case code === codes.plusSign:
+ case code === codes.underscore:
+ effects.consume(code);
+ return inside;
+ default:
+ return nok(code);
+ }
+ }
+ function begin(code) {
+ switch (code) {
+ case codes.eof:
+ case codes.colon:
+ return nok(code);
+ default:
+ effects.consume(code);
+ return inside;
+ }
+ }
+ const start = code => {
+ effects.enter("gemoji");
+ effects.consume(code);
+ return begin;
+ };
+ return start;
+ }
+};
+export {shortcode_default as default};
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 8: http-assert.cjs 1`] = `
+""use strict";
+module.exports = module.exports.default = function (username, check) {
+ try {} catch (err) {}
+};
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 9: human-readable.mjs 1`] = `
+"async function humanReadable(seconds) {
+ return new Promise(resolve => {
+ let formatted = "";
+ for (const converter of [3600, 60, 1]) {
+ const time = seconds / converter | 0;
+ formatted += time < 10 ? \`0\${time}\` : time;
+ if (converter !== 1) formatted += ":";
+ seconds -= time * converter;
+ }
+ return resolve(formatted);
+ });
+}
+export default humanReadable;
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 10: multiply.cjs 1`] = `
+""use strict";
+const multiply = function (a, b) {
+ return a * b;
+};
+module.exports = module.exports.default = multiply;
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 11: noop.mjs 1`] = `
+"export default () => void 0;
+"
+`;
+
+exports[`integration:unassert > remove assertions > program sample 12: subtract.mjs 1`] = `
+"function subtract(a, b) {
+ return a - b;
+}
+export default subtract;
+"
+`;
diff --git a/src/__tests__/unassert.integration.spec.ts b/src/__tests__/unassert.integration.spec.ts
new file mode 100644
index 0000000..663b434
--- /dev/null
+++ b/src/__tests__/unassert.integration.spec.ts
@@ -0,0 +1,45 @@
+/**
+ * @file Integration Tests - unassert
+ * @module estree-util-unassert/tests/integration/unassert
+ */
+
+import type { Options } from '#src/interfaces'
+import pathe from '@flex-development/pathe'
+import type { Nilable } from '@flex-development/tutils'
+import { fromJs } from 'esast-util-from-js'
+import type { Program } from 'estree'
+import { toJs } from 'estree-util-to-js'
+import { read } from 'to-vfile'
+import type { VFile } from 'vfile'
+import testSubject from '../unassert'
+
+describe('integration:unassert', () => {
+ describe('remove assertions', () => {
+ it.each<[string, Nilable?]>([
+ ['add.mjs'],
+ ['assert.mjs'],
+ ['call-expression.mjs'],
+ ['century-from-year.mjs'],
+ ['digitize.mjs'],
+ ['divide.cjs'],
+ ['gemoji-html.mjs'],
+ ['gemoji-shortcode.mjs'],
+ ['http-assert.cjs', { modules: /^(?:http-assert|node:assert)$/ }],
+ ['human-readable.mjs'],
+ ['multiply.cjs'],
+ ['noop.mjs'],
+ ['subtract.mjs']
+ ])('program sample %#: %s', async (basename, options) => {
+ // Arrange
+ const path: string = pathe.resolve('__fixtures__', 'modules', basename)
+ const file: VFile = await read(path)
+ const tree: Program = fromJs(String(file), { module: true })
+
+ // Act
+ testSubject(tree, options)
+
+ // Expect
+ expect(toJs(tree).value).toMatchSnapshot()
+ })
+ })
+})
diff --git a/src/handlers/__tests__/call-expression.functional.spec.ts b/src/handlers/__tests__/call-expression.functional.spec.ts
index 28453ec..fe7a421 100644
--- a/src/handlers/__tests__/call-expression.functional.spec.ts
+++ b/src/handlers/__tests__/call-expression.functional.spec.ts
@@ -322,25 +322,12 @@ describe('functional:handlers/CallExpression', () => {
type: 'ExpressionStatement'
}
- context.grandparent = {
- alternate: null,
- consequent: context.parent,
- test: notTrue,
- type: 'IfStatement'
- }
-
TestSubject.call(context, ok, 'expression', undefined)
})
it('should add parent to trash', () => {
expect(context.trash.has(context.parent!)).to.be.true
})
-
- describe('is(grandparent, "IfStatement")', () => {
- it('should add grandparent to trash if !grandparent.alternate', () => {
- expect(context.trash.has(context.grandparent!)).to.be.true
- })
- })
})
describe('is(parent, "ExportDefaultDeclaration")', () => {
diff --git a/src/handlers/call-expression.ts b/src/handlers/call-expression.ts
index b17d5b5..cdc7313 100644
--- a/src/handlers/call-expression.ts
+++ b/src/handlers/call-expression.ts
@@ -127,10 +127,6 @@ function CallExpression(
break
case 'ExpressionStatement':
this.trash.add(this.parent)
-
- if (is(this.grandparent, 'IfStatement')) {
- !this.grandparent.alternate && this.trash.add(this.grandparent)
- }
break
case 'ExportDefaultDeclaration':
define(this.parent, key, { value: at(node.arguments, 0, void0) })
diff --git a/src/index.ts b/src/index.ts
index e19f4a6..cc79e87 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,4 +3,5 @@
* @module estree-util-unassert
*/
-export {}
+export type { Options } from './interfaces'
+export { default as unassert } from './unassert'
diff --git a/src/interfaces/options.ts b/src/interfaces/options.ts
index bdf7b47..37d05f8 100644
--- a/src/interfaces/options.ts
+++ b/src/interfaces/options.ts
@@ -11,8 +11,8 @@ import type { FilterPattern } from '@rollup/pluginutils'
*/
interface Options {
/**
- * A valid [`picomatch`][1] glob pattern, or array of patterns, matching
- * assertion module ids.
+ * A regular expression, valid [`picomatch`][1] glob pattern, or array of
+ * patterns, matching assertion module ids.
*
* [1]: https://github.com/micromatch/picomatch
*
diff --git a/src/unassert.ts b/src/unassert.ts
new file mode 100644
index 0000000..a4bade6
--- /dev/null
+++ b/src/unassert.ts
@@ -0,0 +1,50 @@
+/**
+ * @file unassert
+ * @module estree-util-unassert/unassert
+ */
+
+import type { Nilable } from '@flex-development/tutils'
+import { createFilter } from '@rollup/pluginutils'
+import type { Node, Program } from 'estree'
+import { visit } from 'estree-util-visit'
+import * as handlers from './handlers'
+import type { HandlerContext, Options } from './interfaces'
+import { MODULES_REGEX } from './utils'
+import * as visitors from './visitors'
+
+/**
+ * Remove assertions.
+ *
+ * @see {@linkcode Options}
+ * @see {@linkcode Program}
+ *
+ * @this {void}
+ *
+ * @param {Program} tree - JavaScript syntax tree
+ * @param {Nilable?} [options] - Configuration options
+ * @return {void} Nothing
+ */
+function unassert(this: void, tree: Program, options?: Nilable): void {
+ options ??= {}
+ options.modules ??= MODULES_REGEX
+
+ /**
+ * Node handler context.
+ *
+ * @const {HandlerContext} context
+ */
+ const context: HandlerContext = {
+ grandparent: null,
+ identifiers: new Set(),
+ modules: createFilter(options.modules, [], { resolve: false }),
+ parent: null,
+ trash: new WeakSet()
+ }
+
+ return void visit(tree, {
+ enter: visitors.enter(context, handlers),
+ leave: visitors.leave(context)
+ })
+}
+
+export default unassert
diff --git a/src/visitors/__tests__/leave.functional.spec.ts b/src/visitors/__tests__/leave.functional.spec.ts
index 3a7c849..42707c6 100644
--- a/src/visitors/__tests__/leave.functional.spec.ts
+++ b/src/visitors/__tests__/leave.functional.spec.ts
@@ -128,7 +128,7 @@ describe('functional:visitors/leave', () => {
})
})
- describe('is(grandparent, "IfStatement")', () => {
+ describe('is(ancestors[i], "IfStatement")', () => {
let ancestors: Node[]
let grandparent: IfStatement
let index: number
diff --git a/src/visitors/leave.ts b/src/visitors/leave.ts
index 30be2ca..7d21a74 100644
--- a/src/visitors/leave.ts
+++ b/src/visitors/leave.ts
@@ -75,8 +75,8 @@ const leave = (context: HandlerContext): Visitor => {
const grandparent: Optional = at(ancestors, -2)
// prepare if statement leave
- if (is(grandparent, 'IfStatement')) {
- preleaveIfStatement(grandparent, context.trash)
+ for (const node of [parent, grandparent]) {
+ is(node, 'IfStatement') && preleaveIfStatement(node, context.trash)
}
// return index of new next node
diff --git a/tsconfig.json b/tsconfig.json
index 85ad18b..6af9582 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -54,16 +54,15 @@
},
"exclude": ["**/coverage", "**/dist", "**/node_modules"],
"include": [
- "**/**.json",
- "**/**.mjs",
- "**/**.mts",
- "**/**.ts",
+ "**/**/*.json",
+ "**/**/*.mjs",
+ "**/**/*.mts",
+ "**/**/*.ts",
"**/.*.json",
"**/.*.mjs",
"**/.*.mts",
"**/.*.ts",
- ".eslintrc*cjs",
- "__fixtures__/*.cjs"
+ ".eslintrc*cjs"
],
"mdx": {
"plugins": [
diff --git a/yarn.lock b/yarn.lock
index c2b7269..908cc32 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1516,10 +1516,12 @@ __metadata:
remark-gfm: "npm:4.0.0"
sh-syntax: "npm:0.4.2"
source-map: "npm:0.8.0-beta.0"
+ to-vfile: "npm:8.0.0"
trash-cli: "npm:5.0.0"
ts-dedent: "npm:2.2.0"
typescript: "npm:5.3.3"
unist-util-is: "npm:6.0.0"
+ vfile: "npm:6.0.1"
vite-tsconfig-paths: "npm:4.3.1"
vitest: "npm:1.3.1"
yaml-eslint-parser: "npm:1.2.2"
@@ -2357,11 +2359,11 @@ __metadata:
linkType: hard
"@types/estree-jsx@npm:^1.0.0":
- version: 1.0.4
- resolution: "@types/estree-jsx@npm:1.0.4"
+ version: 1.0.5
+ resolution: "@types/estree-jsx@npm:1.0.5"
dependencies:
"@types/estree": "npm:*"
- checksum: 10/fb97b3226814e833689304759d8bac29d869ca4cfcfa36f2f3877fb9819f218a11396a28963607e1d0cc72363c3803bfe9a8b16a42924819824e63d10ec386db
+ checksum: 10/a028ab0cd7b2950168a05c6a86026eb3a36a54a4adfae57f13911d7b49dffe573d9c2b28421b2d029b49b3d02fcd686611be2622dc3dad6d9791166c083f6008
languageName: node
linkType: hard
@@ -10027,6 +10029,15 @@ __metadata:
languageName: node
linkType: hard
+"to-vfile@npm:8.0.0":
+ version: 8.0.0
+ resolution: "to-vfile@npm:8.0.0"
+ dependencies:
+ vfile: "npm:^6.0.0"
+ checksum: 10/95552e5c9158e65762cc1ce341f55e9b1ae3267e5a8fb3fa18f5710b588c51b03088c5011aef0a60ac334392cfee142ff4da132234bdfc9da822f3db5cbc8d81
+ languageName: node
+ linkType: hard
+
"tr46@npm:^1.0.1":
version: 1.0.1
resolution: "tr46@npm:1.0.1"
@@ -10632,7 +10643,7 @@ __metadata:
languageName: node
linkType: hard
-"vfile@npm:^6.0.0, vfile@npm:^6.0.1":
+"vfile@npm:6.0.1, vfile@npm:^6.0.0, vfile@npm:^6.0.1":
version: 6.0.1
resolution: "vfile@npm:6.0.1"
dependencies: