Skip to content

Commit

Permalink
feat(ast_tools): add #[builder(default)] attribute for structs and …
Browse files Browse the repository at this point in the history
…enums (#9203)

Add a `#[builder(default)]` attribute which can be used on structs or enums to indicate that AST builder methods should not require this field.

Use that attribute on `ScopeId`, `SymbolId` and `ReferenceId`, to replace the hard-coded list of semantic ID types in codegen for `AstBuilder`.

Does not affect generated code, only the mechanisms by which it's generated.
  • Loading branch information
overlookmotel committed Feb 18, 2025
1 parent be27164 commit 3e7b21c
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 12 deletions.
13 changes: 12 additions & 1 deletion crates/oxc_ast_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,18 @@ pub fn ast_meta(_args: TokenStream, input: TokenStream) -> TokenStream {
/// See [`macro@ast`] for further details.
#[proc_macro_derive(
Ast,
attributes(clone_in, content_eq, estree, generate_derive, plural, scope, span, ts, visit)
attributes(
builder,
clone_in,
content_eq,
estree,
generate_derive,
plural,
scope,
span,
ts,
visit
)
)]
pub fn ast_derive(_input: TokenStream) -> TokenStream {
TokenStream::new()
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_syntax/src/reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use oxc_ast_macros::ast;

#[ast]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[builder(default)]
#[clone_in(default)]
#[content_eq(skip)]
#[estree(skip)]
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_syntax/src/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use oxc_ast_macros::ast;

#[ast]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[builder(default)]
#[clone_in(default)]
#[content_eq(skip)]
#[estree(skip)]
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_syntax/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use oxc_ast_macros::ast;

#[ast]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[builder(default)]
#[clone_in(default)]
#[content_eq(skip)]
#[estree(skip)]
Expand Down
74 changes: 63 additions & 11 deletions tasks/ast_tools/src/generators/ast_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,42 @@ use crate::{
output::{output_path, Output},
schema::{Def, EnumDef, FieldDef, Schema, StructDef, TypeDef, VariantDef},
utils::{create_safe_ident, is_reserved_name},
Codegen, Generator, AST_CRATE_PATH,
Codegen, Generator, Result, AST_CRATE_PATH,
};

use super::define_generator;
use super::{attr_positions, define_generator, AttrLocation, AttrPart, AttrPositions};

/// Types to omit builder method for.
const BLACK_LIST: [&str; 1] = ["Span"];

/// Semantic ID types.
/// We generate builder methods both with and without these fields for types which include any of them.
const SEMANTIC_ID_TYPES: [&str; 3] = ["ScopeId", "SymbolId", "ReferenceId"];

/// Generator for `AstBuilder`.
pub struct AstBuilderGenerator;

define_generator!(AstBuilderGenerator);

impl Generator for AstBuilderGenerator {
/// Register that accept `#[builder]` attr on structs or enums.
fn attrs(&self) -> &[(&'static str, AttrPositions)] {
&[("builder", attr_positions!(Struct | Enum))]
}

/// Parse `#[builder(default)]` on struct or enum.
fn parse_attr(&self, _attr_name: &str, location: AttrLocation, part: AttrPart) -> Result<()> {
// No need to check attr name is `builder`, because that's the only attribute that
// this generator handles.
if !matches!(part, AttrPart::Tag("default")) {
return Err(());
}

match location {
AttrLocation::Struct(struct_def) => struct_def.builder.is_default = true,
AttrLocation::Enum(enum_def) => enum_def.builder.is_default = true,
_ => return Err(()),
}

Ok(())
}

/// Generate `AstBuilder`.
fn generate(&self, schema: &Schema, _codegen: &Codegen) -> Output {
let fns = schema
Expand Down Expand Up @@ -116,7 +134,7 @@ fn generate_builder_methods(type_def: &TypeDef, schema: &Schema) -> TokenStream
fn generate_builder_methods_for_struct(struct_def: &StructDef, schema: &Schema) -> TokenStream {
let (mut params, generic_params, where_clause, has_default_fields) =
get_struct_params(struct_def, schema);
let (fn_params, fields) = get_struct_fn_params_and_fields(&params, true);
let (fn_params, fields) = get_struct_fn_params_and_fields(&params, true, schema);

let (fn_name_postfix, doc_postfix) = if has_default_fields {
let default_params = params.iter().filter(|param| param.is_default);
Expand Down Expand Up @@ -153,7 +171,7 @@ fn generate_builder_methods_for_struct(struct_def: &StructDef, schema: &Schema)
}

// Generate builder functions excluding default fields
let (fn_params, fields) = get_struct_fn_params_and_fields(&params, false);
let (fn_params, fields) = get_struct_fn_params_and_fields(&params, false, schema);
params.retain(|param| !param.is_default);
let mut output2 = generate_builder_methods_for_struct_impl(
struct_def,
Expand Down Expand Up @@ -267,7 +285,13 @@ fn get_struct_params<'s>(
let type_def = field.type_def(schema);
let ty = type_def.ty(schema);

let is_default = SEMANTIC_ID_TYPES.contains(&type_def.innermost_type(schema).name());
// A field is default if its innermost type is marked `#[builder(default)]`
let innermost_type = type_def.innermost_type(schema);
let is_default = match innermost_type {
TypeDef::Struct(inner_struct) => inner_struct.builder.is_default,
TypeDef::Enum(inner_enum) => inner_enum.builder.is_default,
_ => false,
};
if is_default {
has_default_fields = true;
};
Expand Down Expand Up @@ -298,7 +322,7 @@ fn get_struct_params<'s>(

let fn_param_ty = if is_default {
assert!(!has_generic_param);
type_def.innermost_type(schema).ty(schema)
innermost_type.ty(schema)
} else if let Some(generic_ident) = generic_ident {
let where_clause_part = quote!( #generic_ident: IntoIn<'a, #ty> );
let generic_ty = quote!( #generic_ident );
Expand Down Expand Up @@ -342,14 +366,20 @@ fn get_struct_params<'s>(
fn get_struct_fn_params_and_fields(
params: &[Param],
include_default_fields: bool,
schema: &Schema,
) -> (/* function params */ TokenStream, /* fields */ TokenStream) {
let mut fields = vec![];
let fn_params = params.iter().filter_map(|param| {
let param_ident = &param.ident;

if param.is_default {
if include_default_fields {
fields.push(quote!( #param_ident: Cell::new(Some(#param_ident)) ));
// Builder functions which take default fields receive the innermost type as param.
// So wrap the param's value in `Cell::new(...)`, or `Some(...)` if necessary.
let field_type = param.field.type_def(schema);
let value = wrap_default_field_value(quote!( #param_ident ), field_type, schema);

fields.push(quote!( #param_ident: #value ));
return Some(&param.fn_param);
}

Expand Down Expand Up @@ -476,6 +506,28 @@ fn enum_variant_builder_name(enum_def: &EnumDef, variant: &VariantDef) -> Ident
format_ident!("{enum_name}_{variant_name}")
}

/// Wrap the value of a default field in `Cell::new(...)` or `Some(...)` if necessary.
///
/// Wrap recursively, moving inwards towards the innermost type.
fn wrap_default_field_value(
value: TokenStream,
type_def: &TypeDef,
schema: &Schema,
) -> TokenStream {
match type_def {
TypeDef::Cell(cell_def) => {
let inner_value = wrap_default_field_value(value, cell_def.inner_type(schema), schema);
quote!( Cell::new(#inner_value) )
}
TypeDef::Option(option_def) => {
let inner_value =
wrap_default_field_value(value, option_def.inner_type(schema), schema);
quote!( Some(#inner_value) )
}
_ => value,
}
}

/// Generate doc comment for function params.
fn generate_doc_comment_for_params(params: &[Param]) -> TokenStream {
if params.is_empty() {
Expand Down
3 changes: 3 additions & 0 deletions tasks/ast_tools/src/schema/defs/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::utils::{create_ident, pluralize};

use super::{
extensions::{
ast_builder::AstBuilderType,
clone_in::CloneInType,
content_eq::ContentEqType,
estree::{ESTreeEnum, ESTreeEnumVariant},
Expand All @@ -33,6 +34,7 @@ pub struct EnumDef {
pub variants: Vec<VariantDef>,
/// For `@inherits` inherited enum variants
pub inherits: Vec<TypeId>,
pub builder: AstBuilderType,
pub visit: VisitEnum,
pub kind: Kind,
pub layout: Layout,
Expand Down Expand Up @@ -63,6 +65,7 @@ impl EnumDef {
generated_derives,
variants,
inherits,
builder: AstBuilderType::default(),
visit: VisitEnum::default(),
kind: Kind::default(),
layout: Layout::default(),
Expand Down
3 changes: 3 additions & 0 deletions tasks/ast_tools/src/schema/defs/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::utils::{create_ident_tokens, pluralize};

use super::{
extensions::{
ast_builder::AstBuilderType,
clone_in::{CloneInStructField, CloneInType},
content_eq::{ContentEqStructField, ContentEqType},
estree::{ESTreeStruct, ESTreeStructField},
Expand All @@ -29,6 +30,7 @@ pub struct StructDef {
pub file_id: FileId,
pub generated_derives: Derives,
pub fields: Vec<FieldDef>,
pub builder: AstBuilderType,
pub visit: VisitStruct,
pub kind: Kind,
pub layout: Layout,
Expand Down Expand Up @@ -57,6 +59,7 @@ impl StructDef {
file_id,
generated_derives,
fields,
builder: AstBuilderType::default(),
visit: VisitStruct::default(),
kind: Kind::default(),
layout: Layout::default(),
Expand Down
6 changes: 6 additions & 0 deletions tasks/ast_tools/src/schema/extensions/ast_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// Details for `AstBuilder` generator on a struct or enum.
#[derive(Default, Debug)]
pub struct AstBuilderType {
/// `true` if should be replaced with default value in AST builder methods
pub is_default: bool,
}
1 change: 1 addition & 0 deletions tasks/ast_tools/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub use meta::MetaType;

/// Extensions to schema for specific derives / generators
pub mod extensions {
pub mod ast_builder;
pub mod clone_in;
pub mod content_eq;
pub mod estree;
Expand Down

0 comments on commit 3e7b21c

Please sign in to comment.