From ffff5301cd41c2078c61f6eec55d7df7c546302c Mon Sep 17 00:00:00 2001 From: Lexus Drumgold Date: Fri, 1 Mar 2024 23:48:39 -0500 Subject: [PATCH] feat(util): `unassert` Signed-off-by: Lexus Drumgold --- .commitlintrc.ts | 1 + .dictionary.txt | 1 + .eslintignore | 1 + .eslintrc.cjs | 22 +- .lintstagedrc.json | 2 +- __fixtures__/modules/add.mjs | 18 ++ __fixtures__/modules/assert.mjs | 5 + __fixtures__/modules/call-expression.mjs | 97 +++++++ __fixtures__/modules/century-from-year.mjs | 10 + __fixtures__/modules/digitize.mjs | 17 ++ __fixtures__/modules/divide.cjs | 13 + __fixtures__/modules/gemoji-html.mjs | 25 ++ __fixtures__/modules/gemoji-shortcode.mjs | 49 ++++ __fixtures__/modules/http-assert.cjs | 14 + __fixtures__/modules/human-readable.mjs | 22 ++ __fixtures__/modules/multiply.cjs | 13 + __fixtures__/modules/noop.mjs | 3 + __fixtures__/modules/subtract.mjs | 9 + build.config.ts | 44 +++ package.json | 2 + src/__snapshots__/unassert.integration.snap | 250 ++++++++++++++++++ src/__tests__/unassert.integration.spec.ts | 45 ++++ .../call-expression.functional.spec.ts | 13 - src/handlers/call-expression.ts | 4 - src/index.ts | 3 +- src/interfaces/options.ts | 4 +- src/unassert.ts | 50 ++++ .../__tests__/leave.functional.spec.ts | 2 +- src/visitors/leave.ts | 4 +- tsconfig.json | 11 +- yarn.lock | 19 +- 31 files changed, 738 insertions(+), 35 deletions(-) create mode 100644 __fixtures__/modules/add.mjs create mode 100644 __fixtures__/modules/assert.mjs create mode 100644 __fixtures__/modules/call-expression.mjs create mode 100644 __fixtures__/modules/century-from-year.mjs create mode 100644 __fixtures__/modules/digitize.mjs create mode 100644 __fixtures__/modules/divide.cjs create mode 100644 __fixtures__/modules/gemoji-html.mjs create mode 100644 __fixtures__/modules/gemoji-shortcode.mjs create mode 100644 __fixtures__/modules/http-assert.cjs create mode 100644 __fixtures__/modules/human-readable.mjs create mode 100644 __fixtures__/modules/multiply.cjs create mode 100644 __fixtures__/modules/noop.mjs create mode 100644 __fixtures__/modules/subtract.mjs create mode 100644 src/__snapshots__/unassert.integration.snap create mode 100644 src/__tests__/unassert.integration.spec.ts create mode 100644 src/unassert.ts 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: