Skip to content

feat(ast_visit): generate VisitJs visitor that skips TypeScript type-space#24499

Merged
graphite-app[bot] merged 1 commit into
mainfrom
visit-js
Jul 14, 2026
Merged

feat(ast_visit): generate VisitJs visitor that skips TypeScript type-space#24499
graphite-app[bot] merged 1 commit into
mainfrom
visit-js

Conversation

@Boshen

@Boshen Boshen commented Jul 14, 2026

Copy link
Copy Markdown
Member

What

Generates a new VisitJs<'a> trait (+ walk_js module) in oxc_ast_visit that traverses only the JavaScript parts of the AST. It skips pure TypeScript type-space nodes (TSType, TSTypeAnnotation, interfaces, type aliases, …) entirely, but still descends into the JavaScript nested inside a small set of TS wrapper nodes: the five expression casts (x as T, x satisfies T, <T>x, x!, f<T>), decorators, enum member initializers, namespace/module bodies, export = expr, and import x = require(..) value aliases.

VisitJs is for visiting the JavaScript parts of a TypeScript AST — it is not a visitor for ASTs where TypeScript has already been transformed out. The TS constructs carrying runtime JS must therefore still be walked, so their walk code remains in the binary; only the pure type grammar is pruned.

Why (part of #24317)

The generated walk_* functions are generic over the visitor (fn walk_foo<'a, V: Visit<'a>>(…)), so every impl Visit that is actually traversed monomorphizes the full transitive closure of walks it reaches — including all the pure-type walk_ts_* functions. Those TS-grammar walks are ~282 KiB (≈23%) of oxlint's visitor machinery, and oxc_linter alone has 44 helper visitors that only care about runtime JavaScript.

Because the generated walk_js_* functions never reference the pure-type walks, an impl VisitJs monomorphizes zero TS type-grammar traversal. A visitor that only inspects JS can switch impl Visitimpl VisitJs and the TS walks drop out of its binary contribution.

@github-actions github-actions Bot added A-ast Area - AST A-ast-tools Area - AST tools labels Jul 14, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 62 untouched benchmarks
⏩ 9 skipped benchmarks1


Comparing visit-js (66f745c) with main (ca4a4a7)

Open in CodSpeed

Footnotes

  1. 9 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Boshen
Boshen marked this pull request as ready for review July 14, 2026 14:45
@Boshen
Boshen requested a review from overlookmotel as a code owner July 14, 2026 14:45

Boshen commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

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 changes, fast-track this PR to the front of the merge queue

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.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a35de7b3f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/oxc_ast_visit/src/generated/visit_js.rs
Comment thread crates/oxc_ast_visit/src/generated/visit_js.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83e68ec388

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/oxc_ast_visit/src/generated/visit_js.rs
Comment thread crates/oxc_ast_visit/src/generated/visit_js.rs
@graphite-app graphite-app Bot added the 0-merge Merge with Graphite Merge Queue label Jul 14, 2026
@graphite-app

graphite-app Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Merge activity

…space (#24499)

## What

Generates a new **`VisitJs<'a>`** trait (+ `walk_js` module) in `oxc_ast_visit` that traverses **only the JavaScript parts** of the AST. It skips pure TypeScript type-space nodes (`TSType`, `TSTypeAnnotation`, interfaces, type aliases, …) entirely, but still descends into the JavaScript nested inside a small set of TS wrapper nodes: the five expression casts (`x as T`, `x satisfies T`, `<T>x`, `x!`, `f<T>`), decorators, enum member initializers, namespace/module bodies, `export = expr`, and `import x = require(..)` value aliases.

`VisitJs` is for visiting the JavaScript parts of a **TypeScript** AST — it is not a visitor for ASTs where TypeScript has already been transformed out. The TS constructs carrying runtime JS must therefore still be walked, so their walk code remains in the binary; only the pure type grammar is pruned.

## Why (part of #24317)

The generated `walk_*` functions are generic over the visitor (`fn walk_foo<'a, V: Visit<'a>>(…)`), so every `impl Visit` that is actually traversed monomorphizes the full transitive closure of walks it reaches — **including all the pure-type `walk_ts_*` functions**. Those TS-grammar walks are ~282 KiB (≈23%) of oxlint's visitor machinery, and `oxc_linter` alone has 44 helper visitors that only care about runtime JavaScript.

Because the generated `walk_js_*` functions never reference the pure-type walks, an `impl VisitJs` monomorphizes **zero** TS type-grammar traversal. A visitor that only inspects JS can switch `impl Visit` → `impl VisitJs` and the TS walks drop out of its binary contribution.
graphite-app Bot pushed a commit that referenced this pull request Jul 14, 2026
…24513)

## What

First consumer of the `VisitJs` trait from #24499: switches `ContextIdentifierVisitor` (`react_compiler_lowering/find_context_identifiers.rs`) from `impl Visit` to `impl VisitJs`.

## Changes

- `impl Visit` → `impl VisitJs`; direct `walk::walk_*` calls → `walk_js::walk_*`.
- Deletes the six hand-written `visit_ts_*` skip-stubs (`visit_ts_type`, `visit_ts_type_annotation`, `visit_ts_type_parameter_instantiation`/`_declaration`, `visit_ts_type_alias_declaration`, `visit_ts_interface_declaration`) — `VisitJs` prunes type-space natively, and these methods no longer exist on the trait.
- Keeps the empty `visit_ts_enum_declaration` / `visit_ts_module_declaration` overrides: enum initializers and namespace bodies are runtime JS which `VisitJs` *does* walk, while the original Babel walker treated them as opaque RawNodes — the stubs preserve exact parity.
- Drops two `visit_ts_this_parameter(...)` calls that were always no-ops (their walk only descended into the type annotation, which the deleted stub discarded).

Behavior is unchanged; the visitor just no longer monomorphizes any TS type-grammar walks. Existing `oxc_react_compiler` tests cover the pruned/kept boundary directly (`typescript_only_constructs_round_trip`, `type_query_casts_are_renamed_with_value_bindings`, `ts_wrapped_assignment_targets_do_not_panic`).

---

🤖 This PR was written with the assistance of Claude Code (Claude Fable 5).
@graphite-app
graphite-app Bot merged commit d05224d into main Jul 14, 2026
29 checks passed
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Jul 14, 2026
@graphite-app
graphite-app Bot deleted the visit-js branch July 14, 2026 16:06
graphite-app Bot pushed a commit that referenced this pull request Jul 15, 2026
…s from Visit to VisitJs (#24532)

Migrate the transform-pipeline `Visit` implementors to `VisitJs` (#24499): `oxc_transformer` (3), `oxc_minifier` (4), `oxc_react_compiler` (1), plus the parser regex example and the coverage driver. Part of #24317; linter half in #24530. Behavior is unchanged.

Together with #24530: `oxlint` −129.4 KiB (−0.94%) stripped, `oxc_transform` napi −16.2 KiB (−0.46%).

## Why each visitor can switch

A visitor is safe on `VisitJs` when its hooks cannot observe anything inside the pruned type grammar — either the hooked node kinds are value-space-only, or the AST it walks cannot contain type nodes at that pipeline stage.

| Visitor | File | Why equivalent |
|---|---|---|
| `PrivateInExpressionDetector` | `crates/oxc_transformer/src/decorator/legacy/mod.rs` | Hooks `visit_private_in_expression` (+`visit_decorators`); `#x in y` is runtime-only and cannot occur in type grammar, even inside decorator expressions carrying casts. |
| `BindingMover` | `crates/oxc_transformer/src/es2017/async_to_generator.rs` | Its formal-parameter overrides visit only pattern + initializer (`BindingPattern` carries no type field — annotations are sibling fields it never touches); runs in the exit phase after TS strip. |
| `UsedInJSXBindingsCollector` | `crates/oxc_transformer/src/jsx/refresh.rs` | Runs on un-stripped TSX, but its hooks (JSX-shaped calls, `visit_jsx_opening_element`, declarations/imports) cannot fire inside type grammar; its hand-written `visit_ts_type_annotation` skip-stub is deleted — `VisitJs` prunes that natively. |
| `Finder` | `crates/oxc_react_compiler/src/react_compiler/entrypoint/program.rs` | Matches functions by `scope_id`; real JS function scopes never live in pure type grammar, and `VisitJs` still walks the places one could hide (enum initializers, namespace bodies, cast inner expressions). |
| `KeepVar` | `crates/oxc_minifier/src/keep_var.rs` | Hooks `visit_statement`/`visit_variable_declaration`; statements and `var` declarations don't exist in type space, and namespace/enum bodies (which can contain them) are still walked by `VisitJs`. |
| `DirectEvalFlagCheck` | `crates/oxc_minifier/src/peephole/mod.rs` | Debug-only; hooks `visit_call_expression` only — call expressions cannot occur in valid type grammar. |
| `LiveDirectEvalCollector` | `crates/oxc_minifier/src/peephole/mod.rs` | Same single call-expression hook; type-space-immune regardless of input. |
| `TestVisitor` | `crates/oxc_minifier/tests/ecmascript/prop_name.rs` | Test helper parsing `SourceType::default()` (plain JS); single non-recursing `visit_object_expression` hook. |
| `RegularExpressionVisitor` | `crates/oxc_parser/examples/regular_expression.rs` | Finds `RegExpLiteral` / `new RegExp(...)`; neither node kind can occur in type grammar. |
| `CheckASTNodes` | `tasks/coverage/src/driver.rs` | Its only active override is `visit_reg_exp_literal` (regex print idempotency); `RegExpLiteral` cannot occur in type space. (The commented-out `visit_span` full-AST check would need `Visit` if ever revived.) |

Left on `Visit`: the minifier's incremental-scoping bookkeeping (`DropDiff`, `OverPruneCheck`, `LiveRefCollector`) — dropping `let x: Foo = …` must unmark the type-space `Foo` reference, which the `peephole::remove_unused_declaration` tests verify (they fail under `VisitJs`; confirmed and reverted during this work). `ChildScopeCollector` also stays: TS type nodes (mapped/conditional/function types) carry real `scope_id`s it must collect.

---

🤖 This PR was written with the assistance of Claude Code (Claude Fable 5).
graphite-app Bot pushed a commit that referenced this pull request Jul 15, 2026
Migrate 33 JS-only `Visit` implementors in `oxc_linter` (plus the 3 in `tasks/rulegen`) to `VisitJs` (#24499). Part of #24317; transformer/minifier half in #24532. Behavior is unchanged — all 1169 rule tests pass with identical snapshots.

Together with #24532: `oxlint` −129.4 KiB (−0.94%) stripped, `oxc_transform` napi −16.2 KiB (−0.46%).

## Why each visitor can switch

A visitor is safe on `VisitJs` when its hooks cannot observe anything inside the pruned type grammar. The three ways that holds here: the hooked node kinds are value-space-only (`return`, `throw`, `await`, calls, real functions — none exist inside `TSType`), the visitor already skipped type space via hand-written `visit_ts_*` stubs, or its type inspection is by direct field access rather than walking.

| Visitor | File (`crates/oxc_linter/src/`) | Why equivalent |
|---|---|---|
| `ReturnStatementFinder` | `rules/eslint/array_callback_return/return_checker.rs` | Hooks only `visit_return_statement`; `return` cannot occur in type grammar. |
| `ComplexityVisitor` | `rules/eslint/complexity.rs` | All scoring hooks are value-space; the one explicit walk into property type annotations is deleted — valid type grammar contains no complexity-scoring constructs (no `?:`/`&&`/`?.`/default params). |
| `StatementCounter` | `rules/eslint/max_statements.rs` | Counts statements via function/class/static-block body hooks; type declarations contain no statements. |
| `YieldBeforeLoopExitFinder` | `rules/eslint/no_constant_condition.rs` | Hooks only `visit_yield_expression`; `yield` cannot occur in type grammar. |
| `LoopFunctionCollector` | `rules/eslint/no_loop_func.rs` | Hooks `visit_function`/`visit_arrow_function_expression`; those nodes never appear in type grammar (`TSFunctionType` is a distinct node). |
| `ReturnStatementFinder` | `rules/eslint/no_promise_executor_return.rs` | `return`-statement hook only. |
| `ThrowFinder` | `rules/eslint/preserve_caught_error.rs` | Hooks only `visit_throw_statement`; `throw` cannot occur in type grammar. |
| `AwaitFinder` | `rules/eslint/require_await.rs` | Hooks `visit_await_expression`/`visit_for_of_statement`; `await`/`for await` are runtime-only. |
| `RequireCallScanner` | `rules/import/newline_after_import.rs` | Matches `require(...)` calls; calls cannot occur in valid type grammar. |
| `ResolveFinder` | `rules/promise/no_multiple_resolved.rs` | `leave_node` records only value-space kinds (new/import/yield/member); its `visit_call_expression` doesn't descend. |
| `ReturnWrapFinder` | `rules/promise/no_return_wrap.rs` | `return`-statement hook only. |
| `AssertionVisitor` | `rules/shared/jest_vitest/expect_expect.rs` | Call/if/block hooks hunting assertion calls; none can fire in type space. |
| `HookScanner`, `BodyScanner` | `rules/shared/jest_vitest/prefer_expect_assertions.rs` | Call + loop/function-body hooks; `expect.hasAssertions()` calls and loops cannot occur in types. |
| `CallLikeExpressionVisitor` | `rules/shared/jest_vitest/prefer_mock_return_shorthand.rs` | call/new/tagged-template/import hooks — value-space only. (Sibling `IdentifierCollectorVisitor` stays on `Visit`: it collects identifiers, which do leak through type space.) |
| `PromiseExpectScanner` | `rules/shared/jest_vitest/valid_expect_in_promise.rs` | call/await hooks. (Sibling `IdentifierFinder` stays on `Visit` for the same reason as above.) |
| `AwaitDetector`, `ExposeAfterAwaitVisitor` | `rules/vue/no_expose_after_await.rs` | await/call hooks. |
| `LifecycleAfterAwaitVisitor` | `rules/vue/no_lifecycle_after_await.rs` | await/call hooks. |
| `WatchAfterAwaitVisitor` | `rules/vue/no_watch_after_await.rs` | await/expression-statement hooks. |
| `ReturnFinder` | `rules/vue/require_direct_export.rs` | `return`-statement hook only. |
| `ReturnVisitor` | `rules/vue/return_in_emits_validator.rs` | function/arrow depth tracking + return hook; none fire for `TSFunctionType`. |
| `DefineOptionsChecker` | `rules/vue/valid_define_options.rs` | Matches `defineOptions()` calls; reads `type_arguments` by field access, not by walking. |
| `LocalReferenceChecker` | `rules/vue/valid_define_options.rs` | Already skipped type space via a hand-written `visit_ts_type` stub ("`as X`/`typeof str` are statically resolvable") — `VisitJs` subsumes it; stub deleted. |
| `ExhaustiveDepsVisitor` | `rules/react/exhaustive_deps.rs` | Already skipped type space via 4 noop `visit_ts_*` stubs (deleted). The `visit_ts_type_query` stub shows `typeof x` deps were deliberately not collected — `VisitJs` preserves exactly that. |
| `ComponentFinder` | `rules/react/no_multi_comp.rs` | Component-defining hooks (class/function/call/object-property/assignment) are all value-space kinds. |
| `MayThrowBeforeHook` | `rules/react/rules_of_hooks.rs` | `enter_node` treats only Call/New/Member kinds as may-throw; type annotations are erased at runtime and cannot throw. |
| `ConstructorAssignmentCollector` | `rules/typescript/class_literal_property_style.rs` | Assignment-expression hook; assignments cannot occur in type grammar. |
| `ReturnStatementChecker` | `rules/typescript/explicit_function_return_type.rs` | `return`-statement hook only. |
| `ExplicitTypesChecker` | `rules/typescript/explicit_module_boundary_types.rs` | Inspects types by direct field access (`.return_type.is_none()`, `is_const_type_reference()`), never by walking; its 3 cast overrides survive on `VisitJs`. The deleted walk into `new` type args could only produce spurious "missing argument type" reports from function-typed type arguments. |
| `AssignmentVisitor` | `rules/typescript/no_unnecessary_parameter_property_assignment.rs` | Assignment hook; RHS casts are unwrapped via `get_inner_expression`, not walker descent. |
| `JsxFinder` | `utils/react.rs` | JSX/createElement hooks; JSX cannot occur in type grammar. All consumers pass value-space function bodies. |
| `ThisExpressionFinder` | `utils/this_expression.rs` | The `this` type is `TSThisType`, which never fires `visit_this_expression`; the only leak path is a `typeof this.X` qualified name, invalid at the entry scopes its two consumers scan, and nested functions are already cut off by the `visit_function` stub. |
| `TestCase`, `State`, `RuleConfig` | `tasks/rulegen/src/main.rs` | Extract value-space data (test `code` strings, schema object literals) via manual recursion/structural matching; `as`/`satisfies` wrappers are unwrapped with `get_inner_expression`, and `VisitJs` still walks those. |

Left on `Visit` (results would change): `consistent_function_scoping`, `branches_sharing_code`, `prefer_arrow_callback`, `object_shorthand`, `class_methods_use_this`, the two jest identifier collectors above — their identifier/`this` hooks fire inside type space today (`typeof this`, type references) and that input feeds reports or autofixes. Also `no_loop_func`'s `NestedFunctionFinder`/`UnsafeReferenceFinder`, which share a `V: Visit<'a>` generic driver with one of them.

---

🤖 This PR was written with the assistance of Claude Code (Claude Fable 5).
camc314 added a commit that referenced this pull request Jul 21, 2026
### 💥 BREAKING CHANGES

- 54cc121 ast: [**BREAKING**] Split `MetaProperty` into `ImportMeta` and
`NewTarget` (#24557) (camc314)

### 🚀 Features

- 4c71560 parser: More friendly error for spread element in dynamic
imports (#24705) (sapphi-red)
- 7b045cd minfier: Drop last break from last switch case (#24673)
(Armano)
- 7d3c178 minifier: Remove unreachable recursive functions (#24125)
(Dunqing)
- 94f99b3 ast: Allow `NONE` to be passed to AST builder methods where
`Option<ArenaVec>` is expected (#24629) (overlookmotel)
- 77230c5 ast: Accept arrays for `ArenaVec` params of AST builder
methods (#24621) (overlookmotel)
- f08b152 allocator: Implement `FromIn` for array to `Vec` conversion
(#24620) (overlookmotel)
- 2338c13 track-memory-allocations: Track heap deallocs, alloc bytes,
and peak growth (#24619) (Boshen)
- 7aa4739 syntax,transformer: Move JSX entity decoder to `oxc_syntax`
(#24617) (camc314)
- 2b097c4 str: Export `Str` as `ArenaStr` (#24604) (overlookmotel)
- 3acf4c1 minifier: Expand switch optimiation to remove empty cases
(#24520) (Armano)
- 129b759 parser: Improve diagnostics for unparenthesized LHS on
exponential expr (#24569) (camc314)
- 4d0c601 minifier: Fold arithmetic over undefined and null operands
(#24485) (Dunqing)
- 91541dd minifier: Drop empty switch statements  (#24527) (Armano)
- d05224d ast_visit: Generate VisitJs visitor that skips TypeScript
type-space (#24499) (Boshen)
- 3d22307 parser: Add `ParseOptions::enable_ident_hashes` (#24491)
(Boshen)

### 🐛 Bug Fixes

- 64c2241 minifier: Align class heritage removal with assumptions
(#24533) (Dunqing)
- 48b59f4 parser: Span ambient generator diagnostics (#24711) (camc314)
- e750a82 ecmascript: Fix false negative for may_have_side_effects on
dynamic property access (#24709) (sapphi-red)
- f145d73 minifier: Guard reordered identifier reads (#24698) (Dunqing)
- a2ef382 isolated-declarations: Reject `window.Symbol` as global symbol
reference (#24689) (camc314)
- b1bcf72 minifier: Invalidate facts for redeclared bindings (#24658)
(Dunqing)
- 921b834 minifier: Don't treat a conditionally-assigned var as
write-once (#24650) (Dunqing)
- 061af1f minifier: Avoid stale pure function summaries (#24636)
(Dunqing)
- 40c2f43 allocator: `Vec::from_array_in` do not allocate zero-length
array (#24628) (overlookmotel)
- 70994ae codegen: Preserve comments before expression operands (#24510)
(Dunqing)
- 7b4baff parser: Reject new import member access (#23459) (camc314)
- 128b385 minifier: Clippy warning with no-debug-assertions (#24547)
(camc314)
- 8421feb parser: Use first `as` span for imported name (#24537)
(leaysgur)
- c517aa0 parser: Reject invalid accessor assertions (#24504) (camc314)

### ⚡ Performance

- 884d9eb parser: Pre-size cover-grammar assignment target buffers
(#24683) (Boshen)
- d3f07a0 diagnostics: Box OxcDiagnosticInner to reduce binary size
(#24665) (Boshen)
- bcc9de0 parser: Defer diagnostic creation until parse exit (#24663)
(Boshen)
- c35d8ab allocator: Mark `ReplaceWith` panic path cold (#24515)
(camc314)
- ba65790 semantic, allocator: Branchless `clone_in` for semantic IDs
(#24564) (overlookmotel)
- 747feec parser: Build AST nodes with the AST builder instead of
cloning (#24540) (Boshen)
- ba35a0d react_compiler: Use IndexVec for dense id-keyed maps (#24549)
(Boshen)
- b685062 react_compiler: Keep small hot-path collections inline
(#24514) (Marius Schulz)
- a149e95 transformer: Outline rare expression exits (#24512) (camc314)
- 7808a6e react_compiler: Make aliasing effects cheap to intern and
clone (#24506) (Marius Schulz)
- 3a36f2a react_compiler: Store AbstractValue reasons as a u16 bitmask
(#24480) (Boshen)
- 1c96753 react_compiler: Use FxHashMap for the lookup-only aliasing
node map (#24490) (Boshen)

Co-authored-by: Cameron <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ast Area - AST A-ast-tools Area - AST tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant