mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(ast_visit): generate VisitJs visitor that skips TypeScript type-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.
This commit is contained in:
6 files changed
+3900
-2
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -2,10 +2,11 @@ mod generated {
|
||||
#[cfg(feature = "serialize")]
|
||||
mod utf8_to_utf16_converter;
|
||||
pub mod visit;
|
||||
pub mod visit_js;
|
||||
pub mod visit_mut;
|
||||
}
|
||||
|
||||
pub use generated::{visit::*, visit_mut::*};
|
||||
pub use generated::{visit::*, visit_js::*, visit_mut::*};
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
pub mod utf8_to_utf16;
|
||||
@@ -26,6 +26,7 @@ mod traverse;
|
||||
mod typescript;
|
||||
mod utf8_to_utf16;
|
||||
mod visit;
|
||||
mod visit_js;
|
||||
|
||||
pub use assert_layouts::AssertLayoutsGenerator;
|
||||
pub use ast_builder::AstBuilderGenerator;
|
||||
@@ -49,6 +50,7 @@ pub use traverse::TraverseGenerator;
|
||||
pub use typescript::TypescriptGenerator;
|
||||
pub use utf8_to_utf16::Utf8ToUtf16ConverterGenerator;
|
||||
pub use visit::VisitGenerator;
|
||||
pub use visit_js::VisitJsGenerator;
|
||||
|
||||
/// Trait to define a generator.
|
||||
pub trait Generator: Runner {
|
||||
|
||||
@@ -1103,7 +1103,9 @@ impl Target {
|
||||
/// Generate code for `enter_node` and `leave_node`.
|
||||
///
|
||||
/// If the type has no `AstKind`, returns a comment for enter, and empty token stream for exit.
|
||||
fn generate_enter_and_leave_node(
|
||||
///
|
||||
/// Also used by generator for `VisitJs` trait.
|
||||
pub fn generate_enter_and_leave_node(
|
||||
type_ident: &Ident,
|
||||
has_kind: bool,
|
||||
is_mut: bool,
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
//! Generator for `VisitJs` trait.
|
||||
//!
|
||||
//! `VisitJs` is a variant of [`Visit`] which 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 (`x as T`, decorators, enum initializers, namespace bodies, `export = expr`,
|
||||
//! `import x = require(..)`).
|
||||
//!
|
||||
//! `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 JavaScript must therefore still be walked, so their walk code remains in the
|
||||
//! binary; only the pure type grammar is pruned.
|
||||
//!
|
||||
//! Because the generated `walk_*` functions in `mod walk_js` never reference the pure-type
|
||||
//! `walk_ts_*` functions, an `impl VisitJs` monomorphizes zero TS type-grammar traversal. This
|
||||
//! removes the TS-grammar walks (~23% of oxlint's visitor machinery) from the binary contribution
|
||||
//! of any visitor that only inspects runtime JavaScript.
|
||||
//!
|
||||
//! [`Visit`]: super::visit
|
||||
|
||||
use cow_utils::CowUtils;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use syn::{Expr, parse_str};
|
||||
|
||||
use crate::{
|
||||
AST_VISIT_CRATE_PATH, Codegen, Generator,
|
||||
output::{Output, output_path},
|
||||
schema::{Def, EnumDef, FieldDef, Schema, StructDef, TypeDef, VecDef},
|
||||
utils::{create_ident, create_ident_tokens},
|
||||
};
|
||||
|
||||
use super::{
|
||||
define_generator,
|
||||
visit::{
|
||||
INLINE_LIMIT, Target, VisitorOutputs, generate_enter_and_leave_node, generate_visit_type,
|
||||
},
|
||||
};
|
||||
|
||||
/// TypeScript wrapper nodes (defined in `crates/oxc_ast/src/ast/ts.rs`) which carry runtime
|
||||
/// JavaScript and must still be walked by `VisitJs`.
|
||||
///
|
||||
/// Every other type defined in `ts.rs` is pruned by `VisitJs`. This list must be transitively
|
||||
/// complete: every TS node on a path from JS-space down to real JS has to be present, including
|
||||
/// intermediate enums/structs, otherwise the JavaScript below a missing entry is silently dropped
|
||||
/// (the equivalence test in `crates/oxc_ast_visit/tests` guards against that).
|
||||
///
|
||||
/// This is an explicit allowlist rather than an automatically-computed predicate on purpose:
|
||||
/// TS nodes such as `TSLiteralType` (`type X = 1`) or `TSInterfaceDeclaration` (`interface Foo {}`)
|
||||
/// structurally reach JavaScript literal/identifier nodes through *type* space, so a "reaches a JS
|
||||
/// node" analysis would wrongly keep the whole of `TSType` and reintroduce exactly the bloat this
|
||||
/// trait removes. `TSEnumMemberName` (kept) and `TSLiteral` (pruned) are structurally identical
|
||||
/// TS enums of JS literal variants — only intent separates them.
|
||||
const VISITED_TS_NODES: &[&str] = &[
|
||||
// Expression casts / assertions — `.expression: Expression`
|
||||
"TSAsExpression",
|
||||
"TSSatisfiesExpression",
|
||||
"TSTypeAssertion",
|
||||
"TSNonNullExpression",
|
||||
"TSInstantiationExpression",
|
||||
// Decorators — `.expression: Expression`
|
||||
"Decorator",
|
||||
// `export = expr` — `.expression: Expression`
|
||||
"TSExportAssignment",
|
||||
// `import x = require('m')` / `import A = B.C` — value aliases with runtime semantics,
|
||||
// the import counterpart of `export =`. (The type-only form is distinguished by
|
||||
// `import_kind`, not grammar.) `TSTypeName`/`TSQualifiedName` are needed here as the
|
||||
// `B.C` reference chain; they are unreachable from any other visited node.
|
||||
"TSImportEqualsDeclaration",
|
||||
"TSModuleReference",
|
||||
"TSExternalModuleReference",
|
||||
"TSTypeName",
|
||||
"TSQualifiedName",
|
||||
// Enums — member `initializer: Option<Expression>` + computed member names
|
||||
"TSEnumDeclaration",
|
||||
"TSEnumBody",
|
||||
"TSEnumMember",
|
||||
"TSEnumMemberName",
|
||||
// Namespaces / modules — `body: Vec<Statement>` + directives
|
||||
"TSModuleDeclaration",
|
||||
"TSModuleDeclarationName",
|
||||
"TSModuleDeclarationBody",
|
||||
"TSModuleBlock",
|
||||
"TSGlobalDeclaration",
|
||||
];
|
||||
|
||||
/// Generator for `VisitJs` trait.
|
||||
pub struct VisitJsGenerator;
|
||||
|
||||
define_generator!(VisitJsGenerator);
|
||||
|
||||
impl Generator for VisitJsGenerator {
|
||||
fn generate(&self, schema: &Schema, _codegen: &Codegen) -> Output {
|
||||
Output::Rust {
|
||||
path: output_path(AST_VISIT_CRATE_PATH, "visit_js.rs"),
|
||||
tokens: generate(schema),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `VisitJs` visits `type_def`.
|
||||
///
|
||||
/// A type is visited if it has a visitor and is either a JavaScript node (not defined in
|
||||
/// `::ast::ts`) or one of the explicitly-allowlisted TS nodes carrying runtime JavaScript.
|
||||
fn type_is_visited(type_def: &TypeDef, schema: &Schema) -> bool {
|
||||
match type_def {
|
||||
TypeDef::Struct(struct_def) => struct_is_visited(struct_def, schema),
|
||||
TypeDef::Enum(enum_def) => enum_is_visited(enum_def, schema),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `VisitJs` visits this struct.
|
||||
fn struct_is_visited(struct_def: &StructDef, schema: &Schema) -> bool {
|
||||
struct_def.visit.has_visitor()
|
||||
&& is_js_node(struct_def.name(), struct_def.file(schema).import_path())
|
||||
}
|
||||
|
||||
/// Returns `true` if `VisitJs` visits this enum.
|
||||
fn enum_is_visited(enum_def: &EnumDef, schema: &Schema) -> bool {
|
||||
enum_def.visit.has_visitor() && is_js_node(enum_def.name(), enum_def.file(schema).import_path())
|
||||
}
|
||||
|
||||
/// A node is visited by `VisitJs` if it's not a TypeScript node (not in `::ast::ts`), or it's an
|
||||
/// allowlisted TS node carrying runtime JavaScript.
|
||||
fn is_js_node(name: &str, import_path: &str) -> bool {
|
||||
import_path != "::ast::ts" || VISITED_TS_NODES.contains(&name)
|
||||
}
|
||||
|
||||
/// Returns `true` if the innermost type of a field / enum variant is visited by `VisitJs`.
|
||||
fn innermost_is_visited(type_def: &TypeDef, schema: &Schema) -> bool {
|
||||
type_is_visited(type_def.innermost_type(schema), schema)
|
||||
}
|
||||
|
||||
/// [`VisitorOutputs`] for generating visitor calls for just the `VisitJs` trait (immutable side).
|
||||
struct JsVisit(TokenStream);
|
||||
|
||||
impl VisitorOutputs for JsVisit {
|
||||
fn gen_each<F: Fn(bool) -> TokenStream>(f: F) -> Self {
|
||||
Self(f(false))
|
||||
}
|
||||
|
||||
fn map<F: Fn(TokenStream, bool) -> TokenStream>(self, f: F) -> Self {
|
||||
Self(f(self.0, false))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate `VisitJs` trait and `walk_js` module.
|
||||
fn generate(schema: &Schema) -> TokenStream {
|
||||
let mut visit_methods = TokenStream::new();
|
||||
let mut walk_fns = TokenStream::new();
|
||||
|
||||
for type_def in &schema.types {
|
||||
match type_def {
|
||||
TypeDef::Struct(struct_def) => {
|
||||
generate_struct_visitor(struct_def, schema, &mut visit_methods, &mut walk_fns);
|
||||
}
|
||||
TypeDef::Enum(enum_def) => {
|
||||
generate_enum_visitor(enum_def, schema, &mut visit_methods, &mut walk_fns);
|
||||
}
|
||||
TypeDef::Vec(vec_def) => {
|
||||
generate_vec_visitor(vec_def, schema, &mut visit_methods, &mut walk_fns);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
//! JavaScript-only visitor
|
||||
//!
|
||||
//! `VisitJs` traverses only the JavaScript parts of the AST, skipping TypeScript type-space
|
||||
//! nodes. See [visitor pattern](https://rust-unofficial.github.io/patterns/patterns/behavioural/visitor.html).
|
||||
//!
|
||||
//! This visitor is for visiting the JavaScript parts of a TypeScript AST — not for ASTs
|
||||
//! where TypeScript has already been transformed out. TS constructs carrying runtime
|
||||
//! JavaScript (enum initializers, namespace bodies, decorators, `x as T` casts,
|
||||
//! `export =`, `import x = require(..)`) are still walked, so their walk code remains
|
||||
//! in the binary; only the pure type grammar is pruned.
|
||||
|
||||
//!@@line_break
|
||||
#![expect(unused_variables)]
|
||||
#![allow(
|
||||
clippy::match_same_arms,
|
||||
clippy::semicolon_if_nothing_returned,
|
||||
clippy::needless_pass_by_ref_mut,
|
||||
clippy::trivially_copy_pass_by_ref,
|
||||
clippy::match_wildcard_for_single_variants,
|
||||
clippy::single_match_else,
|
||||
)]
|
||||
|
||||
///@@line_break
|
||||
use std::cell::Cell;
|
||||
|
||||
///@@line_break
|
||||
use oxc_allocator::ArenaVec;
|
||||
use oxc_syntax::scope::{ScopeFlags, ScopeId};
|
||||
|
||||
///@@line_break
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ast::ast_kind::AstKind;
|
||||
|
||||
///@@line_break
|
||||
use walk_js::*;
|
||||
|
||||
///@@line_break
|
||||
/// JavaScript-only syntax tree traversal.
|
||||
///
|
||||
/// Like [`Visit`], but skips TypeScript type-space nodes. Still descends into JavaScript
|
||||
/// nested inside TS wrapper nodes (`x as T`, decorators, enum initializers, namespace
|
||||
/// bodies, `export = expr`, `import x = require(..)`).
|
||||
///
|
||||
/// This trait is for visiting the JavaScript parts of a TypeScript AST — not for ASTs
|
||||
/// where TypeScript has already been transformed out. The walks for those JS-carrying
|
||||
/// TS nodes stay in the binary; only the pure type grammar is pruned.
|
||||
///
|
||||
/// Pruning is by grammar (node kind), not by TypeScript's erasure semantics: type-only
|
||||
/// imports/exports (`import type`, `export type`) and `declare`d items are JS-grammar
|
||||
/// nodes and are still visited — filter on `import_kind` / `declare` in the visitor if
|
||||
/// needed, exactly as with [`Visit`].
|
||||
///
|
||||
/// [`Visit`]: crate::Visit
|
||||
pub trait VisitJs<'a>: Sized {
|
||||
#[inline]
|
||||
fn enter_node(&mut self, kind: AstKind<'a>) {}
|
||||
#[inline]
|
||||
fn leave_node(&mut self, kind: AstKind<'a>) {}
|
||||
|
||||
///@@line_break
|
||||
#[inline]
|
||||
fn enter_scope(&mut self, flags: ScopeFlags, scope_id: &Cell<Option<ScopeId>>) {}
|
||||
#[inline]
|
||||
fn leave_scope(&mut self) {}
|
||||
|
||||
///@@line_break
|
||||
#[inline]
|
||||
fn alloc<T>(&self, t: &T) -> &'a T {
|
||||
///@ SAFETY:
|
||||
///@ This should be safe as long as `src` is an reference from the allocator.
|
||||
///@ But honestly, I'm not really sure if this is safe.
|
||||
unsafe {
|
||||
std::mem::transmute(t)
|
||||
}
|
||||
}
|
||||
|
||||
#visit_methods
|
||||
}
|
||||
|
||||
///@@line_break
|
||||
pub mod walk_js {
|
||||
use super::*;
|
||||
|
||||
///@@line_break
|
||||
#walk_fns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate `visit_*` method and `walk_*` function for a struct.
|
||||
fn generate_struct_visitor(
|
||||
struct_def: &StructDef,
|
||||
schema: &Schema,
|
||||
visit_methods: &mut TokenStream,
|
||||
walk_fns: &mut TokenStream,
|
||||
) {
|
||||
// Exit if this struct is not visited by `VisitJs` (not visited at all, or a pure-type TS node)
|
||||
if !struct_is_visited(struct_def, schema) {
|
||||
return;
|
||||
}
|
||||
let Some(visitor_names) = &struct_def.visit.visitor_names else { return };
|
||||
|
||||
let struct_ty = struct_def.ty(schema);
|
||||
let visit_fn_ident = visitor_names.visitor_ident();
|
||||
let walk_fn_ident = visitor_names.walk_ident();
|
||||
|
||||
// Get additional params (e.g. `flags: ScopeFlags`)
|
||||
let (extra_params, extra_args): (TokenStream, TokenStream) = struct_def
|
||||
.visit
|
||||
.visit_args
|
||||
.iter()
|
||||
.map(|(arg_name, arg_type_name)| {
|
||||
let param_ident = create_ident(arg_name);
|
||||
let arg_type_ident = create_ident(arg_type_name);
|
||||
(quote!( , #param_ident: #arg_type_ident ), quote!( , #param_ident ))
|
||||
})
|
||||
.unzip();
|
||||
|
||||
visit_methods.extend(quote! {
|
||||
///@@line_break
|
||||
#[inline]
|
||||
fn #visit_fn_ident(&mut self, it: &#struct_ty #extra_params) {
|
||||
#walk_fn_ident(self, it #extra_args);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate `enter_node` / `leave_node` calls (if this struct has an `AstKind`)
|
||||
let struct_ident = struct_def.ident();
|
||||
let has_kind = struct_def.kind.has_kind;
|
||||
let (enter_node, leave_node) = generate_enter_and_leave_node(&struct_ident, has_kind, false);
|
||||
|
||||
// Generate `enter_scope` / `leave_scope` calls (if this struct has a scope)
|
||||
let (mut scope_entry, mut scope_exit) = if let Some(scope) = &struct_def.visit.scope {
|
||||
let mut flags = parse_str::<Expr>(&scope.flags).unwrap().to_token_stream();
|
||||
if let Some(strict_if) = &scope.strict_if {
|
||||
let strict_if = parse_str::<Expr>(&strict_if.cow_replace("self", "it")).unwrap();
|
||||
flags = quote! {{
|
||||
let mut flags = #flags;
|
||||
if #strict_if {
|
||||
flags |= ScopeFlags::StrictMode;
|
||||
}
|
||||
flags
|
||||
}}
|
||||
}
|
||||
let enter_scope = quote!( visitor.enter_scope(#flags, &it.scope_id); );
|
||||
let leave_scope = quote!( visitor.leave_scope(); );
|
||||
(
|
||||
Some((scope.enter_before_index, enter_scope)),
|
||||
Some((scope.exit_before_index, leave_scope)),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// Generate `visit_*` calls for struct fields, skipping TS-typed fields
|
||||
let mut field_visits_count = 0usize;
|
||||
let mut field_visits = TokenStream::new();
|
||||
for (field_index, field) in struct_def.fields.iter().enumerate() {
|
||||
if let Some(visit) = generate_struct_field_visit(
|
||||
field,
|
||||
field_index,
|
||||
&mut scope_entry,
|
||||
&mut scope_exit,
|
||||
schema,
|
||||
) {
|
||||
field_visits.extend(visit);
|
||||
field_visits_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If didn't enter or exit scope already, enter/exit after last field
|
||||
if let Some((_, enter_scope)) = scope_entry {
|
||||
field_visits.extend(enter_scope);
|
||||
}
|
||||
if let Some((_, leave_scope)) = scope_exit {
|
||||
field_visits.extend(leave_scope);
|
||||
}
|
||||
|
||||
let maybe_inline_attr =
|
||||
if field_visits_count <= INLINE_LIMIT { quote!( #[inline] ) } else { quote!() };
|
||||
|
||||
walk_fns.extend(quote! {
|
||||
///@@line_break
|
||||
#maybe_inline_attr
|
||||
pub fn #walk_fn_ident<'a, V: VisitJs<'a>>(visitor: &mut V, it: &#struct_ty #extra_params) {
|
||||
#enter_node
|
||||
#field_visits
|
||||
#leave_node
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Generate visitor call for a struct field, or `None` if the field is not visited by `VisitJs`.
|
||||
///
|
||||
/// Also inserts `enter_scope` / `leave_scope` calls before the visit call if needed.
|
||||
fn generate_struct_field_visit(
|
||||
field: &FieldDef,
|
||||
field_index: usize,
|
||||
scope_entry: &mut Option<(usize, TokenStream)>,
|
||||
scope_exit: &mut Option<(usize, TokenStream)>,
|
||||
schema: &Schema,
|
||||
) -> Option<TokenStream> {
|
||||
let field_type = field.type_def(schema);
|
||||
|
||||
// Skip fields whose innermost type is a TS type node `VisitJs` doesn't visit
|
||||
// (e.g. `return_type`, `type_parameters`, `type_annotation`). `Vec<Decorator>` is kept,
|
||||
// because `Decorator` is visited.
|
||||
if !innermost_is_visited(field_type, schema) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let field_ident = field.ident();
|
||||
let JsVisit(mut visit) = generate_visit_type(
|
||||
field_type,
|
||||
&Target::Property(quote!( it.#field_ident )),
|
||||
&field.visit.visit_args,
|
||||
&field_ident,
|
||||
"e!(visitor),
|
||||
true,
|
||||
schema,
|
||||
)?;
|
||||
|
||||
// Insert `leave_scope` / `enter_scope` before this field if scope boundary falls here.
|
||||
// Handle exit first, so entering and exiting on the same field produces `enter` before `leave`.
|
||||
if let Some((exit_index, _)) = scope_exit
|
||||
&& *exit_index <= field_index
|
||||
{
|
||||
let (_, leave_scope) = scope_exit.take().unwrap();
|
||||
visit = quote!( #leave_scope #visit );
|
||||
}
|
||||
|
||||
if let Some((enter_index, _)) = scope_entry
|
||||
&& *enter_index <= field_index
|
||||
{
|
||||
let (_, enter_scope) = scope_entry.take().unwrap();
|
||||
visit = quote!( #enter_scope #visit );
|
||||
}
|
||||
|
||||
Some(visit)
|
||||
}
|
||||
|
||||
/// Generate `visit_*` method and `walk_*` function for an enum.
|
||||
fn generate_enum_visitor(
|
||||
enum_def: &EnumDef,
|
||||
schema: &Schema,
|
||||
visit_methods: &mut TokenStream,
|
||||
walk_fns: &mut TokenStream,
|
||||
) {
|
||||
// Exit if this enum is not visited by `VisitJs`
|
||||
if !enum_is_visited(enum_def, schema) {
|
||||
return;
|
||||
}
|
||||
let Some(visitor_names) = &enum_def.visit.visitor_names else { return };
|
||||
|
||||
let enum_ty = enum_def.ty(schema);
|
||||
let visit_fn_ident = visitor_names.visitor_ident();
|
||||
let walk_fn_ident = visitor_names.walk_ident();
|
||||
|
||||
visit_methods.extend(quote! {
|
||||
///@@line_break
|
||||
#[inline]
|
||||
fn #visit_fn_ident(&mut self, it: &#enum_ty) {
|
||||
#walk_fn_ident(self, it);
|
||||
}
|
||||
});
|
||||
|
||||
let enum_ident = enum_def.ident();
|
||||
let (enter_node, leave_node) = generate_enter_and_leave_node(&enum_ident, false, false);
|
||||
|
||||
let mut match_arm_count = 0usize;
|
||||
|
||||
// Own variants, skipping those whose inner type is a TS type node
|
||||
let variant_match_arms = enum_def
|
||||
.variants
|
||||
.iter()
|
||||
.filter_map(|variant| {
|
||||
let variant_type = variant.field_type(schema)?;
|
||||
if !innermost_is_visited(variant_type, schema) {
|
||||
return None;
|
||||
}
|
||||
let JsVisit(visit) = generate_visit_type(
|
||||
variant_type,
|
||||
&Target::Reference(create_ident_tokens("it")),
|
||||
&variant.visit.visit_args,
|
||||
&create_ident_tokens("it"),
|
||||
"e!(visitor),
|
||||
false,
|
||||
schema,
|
||||
)?;
|
||||
|
||||
match_arm_count += 1;
|
||||
|
||||
let variant_ident = variant.ident();
|
||||
Some(quote!( #enum_ident::#variant_ident(it) => #visit, ))
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
// Inherited enums, skipping any which `VisitJs` doesn't visit
|
||||
let inherits_match_arms = enum_def
|
||||
.inherits_enums(schema)
|
||||
.filter_map(|inherits_type| {
|
||||
let inner_visit_fn_ident = inherits_type.visit.visitor_ident()?;
|
||||
if !enum_is_visited(inherits_type, schema) {
|
||||
return None;
|
||||
}
|
||||
|
||||
match_arm_count += 1;
|
||||
|
||||
let inherits_snake_name = inherits_type.snake_name();
|
||||
let match_ident = format_ident!("match_{inherits_snake_name}");
|
||||
let to_fn_ident = format_ident!("to_{inherits_snake_name}");
|
||||
Some(quote! {
|
||||
#match_ident!(#enum_ident) => visitor.#inner_visit_fn_ident(it.#to_fn_ident()),
|
||||
})
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
// Add catch-all match arm if not all variants are visited
|
||||
let catch_all_match_arm = if match_arm_count < enum_def.variants.len() + enum_def.inherits.len()
|
||||
{
|
||||
quote!( _ => {} )
|
||||
} else {
|
||||
quote!()
|
||||
};
|
||||
|
||||
let maybe_inline_attr =
|
||||
if match_arm_count <= INLINE_LIMIT { quote!( #[inline] ) } else { quote!() };
|
||||
|
||||
walk_fns.extend(quote! {
|
||||
///@@line_break
|
||||
#maybe_inline_attr
|
||||
pub fn #walk_fn_ident<'a, V: VisitJs<'a>>(visitor: &mut V, it: &#enum_ty) {
|
||||
#enter_node
|
||||
match it {
|
||||
#variant_match_arms
|
||||
#inherits_match_arms
|
||||
#catch_all_match_arm
|
||||
}
|
||||
#leave_node
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Generate `visit_*` method and `walk_*` function for a `Vec`.
|
||||
fn generate_vec_visitor(
|
||||
vec_def: &VecDef,
|
||||
schema: &Schema,
|
||||
visit_methods: &mut TokenStream,
|
||||
walk_fns: &mut TokenStream,
|
||||
) {
|
||||
// Exit if this `Vec` does not have its own visitor, or its element type is not visited by `VisitJs`
|
||||
let Some(visitor_names) = &vec_def.visit.visitor_names else { return };
|
||||
let inner_type = vec_def.inner_type(schema);
|
||||
if !type_is_visited(inner_type, schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
let vec_ty = vec_def.ty(schema);
|
||||
let visit_fn_ident = visitor_names.visitor_ident();
|
||||
let walk_fn_ident = visitor_names.walk_ident();
|
||||
|
||||
visit_methods.extend(quote! {
|
||||
///@@line_break
|
||||
#[inline]
|
||||
fn #visit_fn_ident(&mut self, it: &#vec_ty) {
|
||||
#walk_fn_ident(self, it);
|
||||
}
|
||||
});
|
||||
|
||||
let inner_visit_fn_ident = match inner_type {
|
||||
TypeDef::Struct(struct_def) => struct_def.visit.visitor_ident().unwrap(),
|
||||
TypeDef::Enum(enum_def) => enum_def.visit.visitor_ident().unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Same `Vec<Argument>` fast-path as `Visit` to reduce stack usage: `Argument` inherits from
|
||||
// `Expression`, so dispatch non-spread elements straight to the expression visitor.
|
||||
let is_arguments_vec =
|
||||
matches!(inner_type, TypeDef::Enum(enum_def) if enum_def.name() == "Argument");
|
||||
|
||||
let loop_body = if is_arguments_vec {
|
||||
quote! {
|
||||
for el in it {
|
||||
match el {
|
||||
oxc_ast::ast::Argument::SpreadElement(spread) => {
|
||||
visitor.visit_spread_element(spread);
|
||||
}
|
||||
_ => {
|
||||
visitor.visit_expression(el.to_expression());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
for el in it {
|
||||
visitor.#inner_visit_fn_ident(el);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk_fns.extend(quote! {
|
||||
///@@line_break
|
||||
#[inline]
|
||||
pub fn #walk_fn_ident<'a, V: VisitJs<'a>>(visitor: &mut V, it: &#vec_ty) {
|
||||
#loop_body
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -266,6 +266,7 @@ const GENERATORS: &[&(dyn Generator + Sync)] = &[
|
||||
&generators::GetIdGenerator,
|
||||
&generators::InheritVariantsGenerator,
|
||||
&generators::VisitGenerator,
|
||||
&generators::VisitJsGenerator,
|
||||
&generators::ScopesCollectorGenerator,
|
||||
&generators::Utf8ToUtf16ConverterGenerator,
|
||||
#[cfg(feature = "generate-js")]
|
||||
|
||||
Reference in new issue
Block a user