Skip to content

Comments

refactor(linter/plugins): ensure debug assertions always shaken out of release build#15946

Merged
graphite-app[bot] merged 1 commit intomainfrom
11-21-refactor_linter_plugins_ensure_debug_assertions_always_shaken_out_of_release_build
Nov 21, 2025
Merged

refactor(linter/plugins): ensure debug assertions always shaken out of release build#15946
graphite-app[bot] merged 1 commit intomainfrom
11-21-refactor_linter_plugins_ensure_debug_assertions_always_shaken_out_of_release_build

Conversation

@overlookmotel
Copy link
Member

@overlookmotel overlookmotel commented Nov 21, 2025

The assert* functions are no-ops in release builds, and minifier removes them as dead code.

However, there's an annoyance - we have to ensure that they're only used in files which end up in the plugins.js chunk. If we use those assertions elsewhere (e.g. in cli.ts), TSDown creates a shared chunk containing them, and then minifier can't see that they can be removed, and leaves all the assertions in the output.

Fix this problem by adding a plugin to TSDown for the release build which replaces imports from asserts.ts with inlined empty functions.

// Original code
import { assertIs, assertIsNonNull } from '../utils/asserts.ts';
// After transform
function assertIs() {}
function assertIsNonNull() {}

This allows us to use the assertion functions anywhere, and minifier can always remove them as dead code.

@github-actions github-actions bot added A-linter Area - Linter A-cli Area - CLI labels Nov 21, 2025
Copy link
Member Author

overlookmotel commented Nov 21, 2025


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions bot added the C-cleanup Category - technical debt or refactoring. Solution not expected to change behavior label Nov 21, 2025
@overlookmotel overlookmotel marked this pull request as ready for review November 21, 2025 14:19
@overlookmotel overlookmotel force-pushed the 11-21-refactor_linter_plugins_ensure_debug_assertions_always_shaken_out_of_release_build branch from 6c71108 to e0f471f Compare November 21, 2025 14:31
@overlookmotel overlookmotel added the A-linter-plugins Area - Linter JS plugins label Nov 21, 2025
@graphite-app graphite-app bot changed the base branch from 11-21-refactor_linter_plugins_move_assertions_into_own_file to graphite-base/15946 November 21, 2025 14:42
graphite-app bot pushed a commit that referenced this pull request Nov 21, 2025
Pure refactor. Split up `plugins/utils.ts` into 3 files in `utils` directory.

Most importantly, assertion functions are by themselves in an `asserts.ts` file. This paves the way for the next PR (#15946).
@graphite-app graphite-app bot force-pushed the 11-21-refactor_linter_plugins_ensure_debug_assertions_always_shaken_out_of_release_build branch from e0f471f to df631d3 Compare November 21, 2025 14:49
@graphite-app graphite-app bot changed the base branch from graphite-base/15946 to main November 21, 2025 14:49
@graphite-app graphite-app bot force-pushed the 11-21-refactor_linter_plugins_ensure_debug_assertions_always_shaken_out_of_release_build branch from df631d3 to 13bc9e2 Compare November 21, 2025 14:50
@camc314
Copy link
Contributor

camc314 commented Nov 21, 2025

@overlookmotel do you have a repro for why this is needed?

I tried the following, on the 11-21-refactor_linter_plugins_move_assertions_into_own_file branch (without this change)

$ git diff
diff --git a/apps/oxlint/src-js/plugins/lint.ts b/apps/oxlint/src-js/plugins/lint.ts
index 70cd9f15e5..517fa4d3c7 100644
--- a/apps/oxlint/src-js/plugins/lint.ts
+++ b/apps/oxlint/src-js/plugins/lint.ts
@@ -79,6 +79,7 @@ function lintFileImpl(
   ruleIds: number[],
   settingsJSON: string,
 ) {
+  assertIsNonNull("");
   // If new buffer, add it to `buffers` array. Otherwise, get existing buffer from array.
   // Do this before checks below, to make sure buffer doesn't get garbage collected when not expected
   // if there's an error.

This yields:

$ rg "function lintFileImpl" ./dist ./debug -A 3
./debug/plugins.js
14632:function lintFileImpl(filePath$1, bufferId, buffer$1, ruleIds, settingsJSON$1) {
14633-  if (assertIsNonNull(""), buffer$1 === null) buffer$1 = buffers[bufferId];
14634-  else {
14635-          let { buffer: arrayBuffer, byteOffset } = buffer$1;

./dist/plugins.js
14624:function lintFileImpl(filePath$1, bufferId, buffer$1, ruleIds, settingsJSON$1) {
14625-  if (buffer$1 === null) buffer$1 = buffers[bufferId];
14626-  else {
14627-          let { buffer: arrayBuffer, byteOffset } = buffer$1;

Only in the debugBuild is assertIsNonNull called.

If we're worred about this regressing, I think it'd be better to write a plugin that executes after everythings done that checks that assertIsNonNull doesn't exist anymore.

Edit: oh nevermind, i see if you import it in cli.ts, it still gets brought in. I think we should try speaking with the rolldown team to get this working. I think i've heard of solutions to this or other people running into the same issue

@camc314 camc314 added the 0-merge Merge with Graphite Merge Queue label Nov 21, 2025
Copy link
Contributor

camc314 commented Nov 21, 2025

Merge activity

graphite-app bot pushed a commit that referenced this pull request Nov 21, 2025
…f release build (#15946)

The `assert*` functions are no-ops in release builds, and minifier removes them as dead code.

However, there's an annoyance - we have to ensure that they're only used in files which end up in the `plugins.js` chunk. If we use those assertions elsewhere (e.g. in `cli.ts`), TSDown creates a shared chunk containing them, and then minifier can't see that they can be removed, and leaves all the assertions in the output.

Fix this problem by adding a plugin to TSDown for the release build which replaces `import`s from `asserts.ts` with inlined empty functions.

```ts
// Original code
import { assertIs, assertIsNonNull } from '../utils/asserts.ts';
```

```ts
// After transform
function assertIs() {}
function assertIsNonNull() {}
```

This allows us to use the assertion functions anywhere, and minifier can always remove them as dead code.
Copilot AI review requested due to automatic review settings November 21, 2025 14:56
@graphite-app graphite-app bot force-pushed the 11-21-refactor_linter_plugins_ensure_debug_assertions_always_shaken_out_of_release_build branch from 13bc9e2 to 4ad085c Compare November 21, 2025 14:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

graphite-app bot pushed a commit that referenced this pull request Nov 21, 2025
After #15946, we're now free to use `assertIsNonNull` in `cli.ts` without breaking dead code removal. So use it instead of unchecked `!` assertions. In debug build it becomes a real runtime assertion.
…f release build (#15946)

The `assert*` functions are no-ops in release builds, and minifier removes them as dead code.

However, there's an annoyance - we have to ensure that they're only used in files which end up in the `plugins.js` chunk. If we use those assertions elsewhere (e.g. in `cli.ts`), TSDown creates a shared chunk containing them, and then minifier can't see that they can be removed, and leaves all the assertions in the output.

Fix this problem by adding a plugin to TSDown for the release build which replaces `import`s from `asserts.ts` with inlined empty functions.

```ts
// Original code
import { assertIs, assertIsNonNull } from '../utils/asserts.ts';
```

```ts
// After transform
function assertIs() {}
function assertIsNonNull() {}
```

This allows us to use the assertion functions anywhere, and minifier can always remove them as dead code.
@graphite-app graphite-app bot force-pushed the 11-21-refactor_linter_plugins_ensure_debug_assertions_always_shaken_out_of_release_build branch from 4ad085c to 152e5de Compare November 21, 2025 14:59
graphite-app bot pushed a commit that referenced this pull request Nov 21, 2025
After #15946, we're now free to use `assertIsNonNull` in `cli.ts` without breaking dead code removal. So use it instead of unchecked `!` assertions. In debug build it becomes a real runtime assertion.
@graphite-app graphite-app bot merged commit 152e5de into main Nov 21, 2025
19 checks passed
@graphite-app graphite-app bot deleted the 11-21-refactor_linter_plugins_ensure_debug_assertions_always_shaken_out_of_release_build branch November 21, 2025 15:05
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Nov 21, 2025
Copilot AI pushed a commit that referenced this pull request Nov 21, 2025
Pure refactor. Split up `plugins/utils.ts` into 3 files in `utils` directory.

Most importantly, assertion functions are by themselves in an `asserts.ts` file. This paves the way for the next PR (#15946).
Copilot AI pushed a commit that referenced this pull request Nov 21, 2025
…f release build (#15946)

The `assert*` functions are no-ops in release builds, and minifier removes them as dead code.

However, there's an annoyance - we have to ensure that they're only used in files which end up in the `plugins.js` chunk. If we use those assertions elsewhere (e.g. in `cli.ts`), TSDown creates a shared chunk containing them, and then minifier can't see that they can be removed, and leaves all the assertions in the output.

Fix this problem by adding a plugin to TSDown for the release build which replaces `import`s from `asserts.ts` with inlined empty functions.

```ts
// Original code
import { assertIs, assertIsNonNull } from '../utils/asserts.ts';
```

```ts
// After transform
function assertIs() {}
function assertIsNonNull() {}
```

This allows us to use the assertion functions anywhere, and minifier can always remove them as dead code.
graphite-app bot pushed a commit that referenced this pull request Nov 21, 2025
After #15946, we're now free to use `assertIsNonNull` in `cli.ts` without breaking dead code removal. So use it instead of unchecked `!` assertions. In debug build it becomes a real runtime assertion.
@overlookmotel
Copy link
Member Author

Only in the debugBuild is assertIsNonNull called.

Yes, that's the idea. But in release build it's removed entirely.

I think we should try speaking with the rolldown team to get this working. I think i've heard of solutions to this or other people running into the same issue

I tried a few things to prevent TSDown producing a shared chunks (manual chunking options etc). But nothing worked.

Looks like it's already tracked here: rolldown/rolldown#4437

graphite-app bot pushed a commit that referenced this pull request Nov 21, 2025
#15946 means we can now simplify the TSDown build config.

Previously we had to have 2 separate builds for `index.ts` and `cli.ts`, to avoid `assert*` functions ending up in a shared chunk, and not getting removed by minifier. Now that problem is solved, so we can switch to a single build with 2 entry points.

TSDown does create a small extra chunk for its `__toESM` and `__commonJSMin` functions, but I think that's fine.

Annoyingly it doesn't seem to be possible to tell TSDown to generate `.d.ts` files only for the `index.js` chunk, so have to delete this pointless files in build script.

The point of all this is that `RuleTester` needs to use a ton of code from `plugins.ts`, and we wouldn't want all that code duplicated in both `index` and `plugins` chunks (especially once we're bundling TS-ESlint's parser #15861). Now that code will be shared between the two entry points in a shared chunk, not duplicated.
taearls pushed a commit to taearls/oxc that referenced this pull request Dec 11, 2025
…15945)

Pure refactor. Split up `plugins/utils.ts` into 3 files in `utils` directory.

Most importantly, assertion functions are by themselves in an `asserts.ts` file. This paves the way for the next PR (oxc-project#15946).
taearls pushed a commit to taearls/oxc that referenced this pull request Dec 11, 2025
…f release build (oxc-project#15946)

The `assert*` functions are no-ops in release builds, and minifier removes them as dead code.

However, there's an annoyance - we have to ensure that they're only used in files which end up in the `plugins.js` chunk. If we use those assertions elsewhere (e.g. in `cli.ts`), TSDown creates a shared chunk containing them, and then minifier can't see that they can be removed, and leaves all the assertions in the output.

Fix this problem by adding a plugin to TSDown for the release build which replaces `import`s from `asserts.ts` with inlined empty functions.

```ts
// Original code
import { assertIs, assertIsNonNull } from '../utils/asserts.ts';
```

```ts
// After transform
function assertIs() {}
function assertIsNonNull() {}
```

This allows us to use the assertion functions anywhere, and minifier can always remove them as dead code.
taearls pushed a commit to taearls/oxc that referenced this pull request Dec 11, 2025
…ject#15947)

After oxc-project#15946, we're now free to use `assertIsNonNull` in `cli.ts` without breaking dead code removal. So use it instead of unchecked `!` assertions. In debug build it becomes a real runtime assertion.
taearls pushed a commit to taearls/oxc that referenced this pull request Dec 11, 2025
oxc-project#15946 means we can now simplify the TSDown build config.

Previously we had to have 2 separate builds for `index.ts` and `cli.ts`, to avoid `assert*` functions ending up in a shared chunk, and not getting removed by minifier. Now that problem is solved, so we can switch to a single build with 2 entry points.

TSDown does create a small extra chunk for its `__toESM` and `__commonJSMin` functions, but I think that's fine.

Annoyingly it doesn't seem to be possible to tell TSDown to generate `.d.ts` files only for the `index.js` chunk, so have to delete this pointless files in build script.

The point of all this is that `RuleTester` needs to use a ton of code from `plugins.ts`, and we wouldn't want all that code duplicated in both `index` and `plugins` chunks (especially once we're bundling TS-ESlint's parser oxc-project#15861). Now that code will be shared between the two entry points in a shared chunk, not duplicated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-linter Area - Linter A-linter-plugins Area - Linter JS plugins C-cleanup Category - technical debt or refactoring. Solution not expected to change behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants