mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
c6cbe42e17
Pure refactor. TSDown config had got very large with the custom plugins to replace `debugAssert` calls, and hoist globals vars to top level. A PR to follow will make the situation worse, as it adds another plugin. Move the plugins into separate files, to bring the TSDown config back to a manageable size. This PR does not alter the plugin code, just moves it.
121 lines
4.4 KiB
TypeScript
121 lines
4.4 KiB
TypeScript
import { join as pathJoin } from "node:path";
|
|
import { Visitor } from "oxc-parser";
|
|
import { parse } from "./utils.ts";
|
|
|
|
import type { Plugin } from "rolldown";
|
|
|
|
// Path to file which defines assertion functions
|
|
const ASSERTS_PATH = pathJoin(import.meta.dirname, "../src-js/utils/asserts.ts");
|
|
|
|
/**
|
|
* Plugin to remove imports of `debugAssert*` / `typeAssert*` functions from `src-js/utils/asserts.ts`,
|
|
* and all their call sites.
|
|
*
|
|
* ```ts
|
|
* // Original code
|
|
* import { debugAssertIsNonNull } from '../utils/asserts.ts';
|
|
* const foo = getFoo();
|
|
* debugAssertIsNonNull(foo.bar);
|
|
*
|
|
* // After transform
|
|
* const foo = getFoo();
|
|
* ```
|
|
*
|
|
* This solves 2 problems:
|
|
*
|
|
* # 1. Minifier works chunk-by-chunk
|
|
*
|
|
* Minifier can already remove all calls to these functions as dead code, but only if the functions are defined
|
|
* in the same file as the call sites.
|
|
*
|
|
* Problem is that `asserts.ts` is imported by files which end up in all output chunks.
|
|
* So without this transform, TSDown creates a shared chunk for `asserts.ts`. Minifier works chunk-by-chunk,
|
|
* so can't see that these functions are no-ops, and doesn't remove the function calls.
|
|
*
|
|
* # 2. Not entirely removed
|
|
*
|
|
* Even if minifier does remove all calls to these functions, it can't prove that expressions *inside* the calls
|
|
* don't have side effects.
|
|
*
|
|
* In example above, it can't know if `foo` has a getter for `bar` property.
|
|
* So it removes the call to `debugAssertIsNonNull`, but leaves behind the `foo.bar` expression.
|
|
*
|
|
* ```ts
|
|
* const foo = getFoo();
|
|
* foo.bar;
|
|
* ```
|
|
*
|
|
* This plugin visits AST and removes all calls to `debugAssert*` / `typeAssert*` functions entirely,
|
|
* *including* the expressions inside the calls.
|
|
*
|
|
* This makes these debug assertion functions act like `debug_assert!` in Rust.
|
|
*/
|
|
const plugin: Plugin = {
|
|
name: "replace-asserts",
|
|
transform: {
|
|
// Only process TS files in `src-js` directory
|
|
filter: { id: /\/src-js\/.+(?<!\.d)\.ts$/ },
|
|
|
|
async handler(code, path, meta) {
|
|
const magicString = meta.magicString!;
|
|
const program = parse(path, code);
|
|
|
|
// Gather names of assertion functions imported from `asserts.ts`.
|
|
// Also gather all identifiers used in the `import` statements, so can avoid erroring on them in visitor below.
|
|
const assertFnNames: Set<string> = new Set(),
|
|
idents = new Set();
|
|
for (const stmt of program.body) {
|
|
if (stmt.type !== "ImportDeclaration") continue;
|
|
|
|
// Check if import is from `utils/asserts.ts`.
|
|
// `endsWith` check is just a shortcut to avoid resolving the specifier to a full path for most imports.
|
|
const source = stmt.source.value;
|
|
if (!source.endsWith("/asserts.ts") && !source.endsWith("/asserts.js")) continue;
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
const importedId = await this.resolve(source, path);
|
|
if (importedId === null || importedId.id !== ASSERTS_PATH) continue;
|
|
|
|
// Remove `import` statement
|
|
for (const specifier of stmt.specifiers) {
|
|
if (specifier.type !== "ImportSpecifier") {
|
|
throw new Error(`Only use named imports when importing from \`asserts.ts\`: ${path}`);
|
|
}
|
|
idents.add(specifier.local);
|
|
if (specifier.imported.type === "Identifier") idents.add(specifier.imported);
|
|
assertFnNames.add(specifier.local.name);
|
|
}
|
|
magicString.remove(stmt.start, stmt.end);
|
|
}
|
|
|
|
if (assertFnNames.size === 0) return;
|
|
|
|
// Visit AST and remove all calls to assertion functions
|
|
const visitor = new Visitor({
|
|
// Replace `debugAssert(...)` calls with `null`. Minifier will remove the `null`.
|
|
CallExpression(node) {
|
|
const { callee } = node;
|
|
if (callee.type !== "Identifier") return;
|
|
if (assertFnNames.has(callee.name)) {
|
|
idents.add(callee);
|
|
magicString.overwrite(node.start, node.end, "null");
|
|
}
|
|
},
|
|
// Error if assertion functions are used in any other way. We lack logic to deal with that.
|
|
Identifier(node) {
|
|
const { name } = node;
|
|
if (assertFnNames.has(name) && !idents.has(node)) {
|
|
throw new Error(
|
|
`Do not use \`${name}\` imported from \`asserts.ts\` except in function calls: ${path}`,
|
|
);
|
|
}
|
|
},
|
|
});
|
|
visitor.visit(program);
|
|
|
|
return { code: magicString };
|
|
},
|
|
},
|
|
};
|
|
|
|
export default plugin;
|