feat(estree): ESTree compatibility for all literals (#7152)

Adds some new estree macro directives:
- `#[estree(via = foo::Foo)`: Uses From to convert this struct to foo::Foo before serialization
- `#[estree(add_ts = "foo: string")]`: Adds additional fields to the typescript definitions

Used these to make all different literals estree-compatible.
This commit is contained in:
ottomated 2024-11-06 21:25:41 +00:00
parent dc0215c906
commit 9d6cc9d3af
15 changed files with 206 additions and 118 deletions

2
Cargo.lock generated
View file

@ -1470,7 +1470,9 @@ name = "oxc_ast"
version = "0.35.0"
dependencies = [
"bitflags 2.6.0",
"cow-utils",
"num-bigint",
"num-traits",
"oxc_allocator",
"oxc_ast_macros",
"oxc_estree",

View file

@ -27,7 +27,9 @@ oxc_span = { workspace = true }
oxc_syntax = { workspace = true }
bitflags = { workspace = true }
cow-utils = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
serde = { workspace = true, features = ["derive"], optional = true }
serde_json = { workspace = true, optional = true }

View file

@ -20,6 +20,7 @@ use oxc_syntax::number::{BigintBase, NumberBase};
#[ast(visit)]
#[derive(Debug, Clone)]
#[generate_derive(CloneIn, GetSpan, GetSpanMut, ContentEq, ContentHash, ESTree)]
#[estree(type = "Literal", via = crate::serialize::ESTreeLiteral, add_ts = "raw: string")]
pub struct BooleanLiteral {
/// Node location in source code
pub span: Span,
@ -33,6 +34,7 @@ pub struct BooleanLiteral {
#[ast(visit)]
#[derive(Debug, Clone)]
#[generate_derive(CloneIn, GetSpan, GetSpanMut, ContentEq, ESTree)]
#[estree(type = "Literal", via = crate::serialize::ESTreeLiteral, add_ts = "value: null, raw: \"null\"")]
pub struct NullLiteral {
/// Node location in source code
pub span: Span,
@ -44,6 +46,7 @@ pub struct NullLiteral {
#[ast(visit)]
#[derive(Debug, Clone)]
#[generate_derive(CloneIn, GetSpan, GetSpanMut, ContentEq, ESTree)]
#[estree(type = "Literal", via = crate::serialize::ESTreeLiteral)]
pub struct NumericLiteral<'a> {
/// Node location in source code
pub span: Span,
@ -60,6 +63,7 @@ pub struct NumericLiteral<'a> {
#[ast(visit)]
#[derive(Debug, Clone)]
#[generate_derive(CloneIn, GetSpan, GetSpanMut, ContentEq, ContentHash, ESTree)]
#[estree(type = "Literal", via = crate::serialize::ESTreeLiteral, add_ts = "value: null, bigint: string")]
pub struct BigIntLiteral<'a> {
/// Node location in source code
pub span: Span,
@ -76,18 +80,20 @@ pub struct BigIntLiteral<'a> {
#[ast(visit)]
#[derive(Debug)]
#[generate_derive(CloneIn, GetSpan, GetSpanMut, ContentEq, ContentHash, ESTree)]
#[estree(
type = "Literal",
via = crate::serialize::ESTreeLiteral,
add_ts = "value: {} | null, regex: { pattern: string, flags: string }"
)]
pub struct RegExpLiteral<'a> {
/// Node location in source code
pub span: Span,
/// Placeholder for printing.
///
/// Valid regular expressions are printed as `{}`, while invalid ones are
/// printed as `null`. Note that invalid regular expressions are not yet
/// printed properly.
pub value: EmptyObject,
/// The parsed regular expression. See [`oxc_regular_expression`] for more
/// details.
#[estree(skip)]
pub regex: RegExp<'a>,
/// The regular expression as it appears in source code
pub raw: &'a str,
}
/// A regular expression
@ -122,13 +128,6 @@ pub enum RegExpPattern<'a> {
Pattern(Box<'a, Pattern<'a>>) = 2,
}
/// An empty object literal (`{}`)
#[ast]
#[derive(Debug, Clone)]
#[generate_derive(CloneIn, ContentEq, ContentHash, ESTree)]
#[estree(no_type)]
pub struct EmptyObject;
/// String literal
///
/// <https://tc39.es/ecma262/#sec-literals-string-literals>

View file

@ -31,11 +31,11 @@ const _: () = {
assert!(offset_of!(BigIntLiteral, raw) == 8usize);
assert!(offset_of!(BigIntLiteral, base) == 24usize);
assert!(size_of::<RegExpLiteral>() == 40usize);
assert!(size_of::<RegExpLiteral>() == 56usize);
assert!(align_of::<RegExpLiteral>() == 8usize);
assert!(offset_of!(RegExpLiteral, span) == 0usize);
assert!(offset_of!(RegExpLiteral, value) == 8usize);
assert!(offset_of!(RegExpLiteral, regex) == 8usize);
assert!(offset_of!(RegExpLiteral, raw) == 40usize);
assert!(size_of::<RegExp>() == 32usize);
assert!(align_of::<RegExp>() == 8usize);
@ -45,9 +45,6 @@ const _: () = {
assert!(size_of::<RegExpPattern>() == 24usize);
assert!(align_of::<RegExpPattern>() == 8usize);
assert!(size_of::<EmptyObject>() == 0usize);
assert!(align_of::<EmptyObject>() == 1usize);
assert!(size_of::<StringLiteral>() == 24usize);
assert!(align_of::<StringLiteral>() == 8usize);
assert!(offset_of!(StringLiteral, span) == 0usize);
@ -1591,11 +1588,11 @@ const _: () = {
assert!(offset_of!(BigIntLiteral, raw) == 8usize);
assert!(offset_of!(BigIntLiteral, base) == 16usize);
assert!(size_of::<RegExpLiteral>() == 24usize);
assert!(size_of::<RegExpLiteral>() == 32usize);
assert!(align_of::<RegExpLiteral>() == 4usize);
assert!(offset_of!(RegExpLiteral, span) == 0usize);
assert!(offset_of!(RegExpLiteral, value) == 8usize);
assert!(offset_of!(RegExpLiteral, regex) == 8usize);
assert!(offset_of!(RegExpLiteral, raw) == 24usize);
assert!(size_of::<RegExp>() == 16usize);
assert!(align_of::<RegExp>() == 4usize);
@ -1605,9 +1602,6 @@ const _: () = {
assert!(size_of::<RegExpPattern>() == 12usize);
assert!(align_of::<RegExpPattern>() == 4usize);
assert!(size_of::<EmptyObject>() == 0usize);
assert!(align_of::<EmptyObject>() == 1usize);
assert!(size_of::<StringLiteral>() == 16usize);
assert!(align_of::<StringLiteral>() == 4usize);
assert!(offset_of!(StringLiteral, span) == 0usize);

View file

@ -159,16 +159,14 @@ impl<'a> AstBuilder<'a> {
///
/// ## Parameters
/// - span: Node location in source code
/// - value: Placeholder for printing.
/// - regex: The parsed regular expression. See [`oxc_regular_expression`] for more
/// - raw: The regular expression as it appears in source code
#[inline]
pub fn reg_exp_literal(
self,
span: Span,
value: EmptyObject,
regex: RegExp<'a>,
) -> RegExpLiteral<'a> {
RegExpLiteral { span, value, regex }
pub fn reg_exp_literal<S>(self, span: Span, regex: RegExp<'a>, raw: S) -> RegExpLiteral<'a>
where
S: IntoIn<'a, &'a str>,
{
RegExpLiteral { span, regex, raw: raw.into_in(self.allocator) }
}
/// Build a [`RegExpLiteral`], and store it in the memory arena.
@ -177,16 +175,19 @@ impl<'a> AstBuilder<'a> {
///
/// ## Parameters
/// - span: Node location in source code
/// - value: Placeholder for printing.
/// - regex: The parsed regular expression. See [`oxc_regular_expression`] for more
/// - raw: The regular expression as it appears in source code
#[inline]
pub fn alloc_reg_exp_literal(
pub fn alloc_reg_exp_literal<S>(
self,
span: Span,
value: EmptyObject,
regex: RegExp<'a>,
) -> Box<'a, RegExpLiteral<'a>> {
Box::new_in(self.reg_exp_literal(span, value, regex), self.allocator)
raw: S,
) -> Box<'a, RegExpLiteral<'a>>
where
S: IntoIn<'a, &'a str>,
{
Box::new_in(self.reg_exp_literal(span, regex, raw), self.allocator)
}
/// Build a [`StringLiteral`].
@ -445,16 +446,19 @@ impl<'a> AstBuilder<'a> {
///
/// ## Parameters
/// - span: Node location in source code
/// - value: Placeholder for printing.
/// - regex: The parsed regular expression. See [`oxc_regular_expression`] for more
/// - raw: The regular expression as it appears in source code
#[inline]
pub fn expression_reg_exp_literal(
pub fn expression_reg_exp_literal<S>(
self,
span: Span,
value: EmptyObject,
regex: RegExp<'a>,
) -> Expression<'a> {
Expression::RegExpLiteral(self.alloc(self.reg_exp_literal(span, value, regex)))
raw: S,
) -> Expression<'a>
where
S: IntoIn<'a, &'a str>,
{
Expression::RegExpLiteral(self.alloc(self.reg_exp_literal(span, regex, raw)))
}
/// Build an [`Expression::StringLiteral`]
@ -7959,16 +7963,19 @@ impl<'a> AstBuilder<'a> {
///
/// ## Parameters
/// - span: Node location in source code
/// - value: Placeholder for printing.
/// - regex: The parsed regular expression. See [`oxc_regular_expression`] for more
/// - raw: The regular expression as it appears in source code
#[inline]
pub fn ts_literal_reg_exp_literal(
pub fn ts_literal_reg_exp_literal<S>(
self,
span: Span,
value: EmptyObject,
regex: RegExp<'a>,
) -> TSLiteral<'a> {
TSLiteral::RegExpLiteral(self.alloc(self.reg_exp_literal(span, value, regex)))
raw: S,
) -> TSLiteral<'a>
where
S: IntoIn<'a, &'a str>,
{
TSLiteral::RegExpLiteral(self.alloc(self.reg_exp_literal(span, regex, raw)))
}
/// Build a [`TSLiteral::StringLiteral`]

View file

@ -60,8 +60,8 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for RegExpLiteral<'old_alloc> {
fn clone_in(&self, allocator: &'new_alloc Allocator) -> Self::Cloned {
RegExpLiteral {
span: CloneIn::clone_in(&self.span, allocator),
value: CloneIn::clone_in(&self.value, allocator),
regex: CloneIn::clone_in(&self.regex, allocator),
raw: CloneIn::clone_in(&self.raw, allocator),
}
}
}
@ -87,13 +87,6 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for RegExpPattern<'old_alloc> {
}
}
impl<'alloc> CloneIn<'alloc> for EmptyObject {
type Cloned = EmptyObject;
fn clone_in(&self, _: &'alloc Allocator) -> Self::Cloned {
EmptyObject
}
}
impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for StringLiteral<'old_alloc> {
type Cloned = StringLiteral<'new_alloc>;
fn clone_in(&self, allocator: &'new_alloc Allocator) -> Self::Cloned {

View file

@ -44,8 +44,8 @@ impl<'a> ContentEq for BigIntLiteral<'a> {
impl<'a> ContentEq for RegExpLiteral<'a> {
fn content_eq(&self, other: &Self) -> bool {
ContentEq::content_eq(&self.value, &other.value)
&& ContentEq::content_eq(&self.regex, &other.regex)
ContentEq::content_eq(&self.regex, &other.regex)
&& ContentEq::content_eq(&self.raw, &other.raw)
}
}
@ -75,12 +75,6 @@ impl<'a> ContentEq for RegExpPattern<'a> {
}
}
impl ContentEq for EmptyObject {
fn content_eq(&self, _: &Self) -> bool {
true
}
}
impl<'a> ContentEq for StringLiteral<'a> {
fn content_eq(&self, other: &Self) -> bool {
ContentEq::content_eq(&self.value, &other.value)

View file

@ -32,8 +32,8 @@ impl<'a> ContentHash for BigIntLiteral<'a> {
impl<'a> ContentHash for RegExpLiteral<'a> {
fn content_hash<H: Hasher>(&self, state: &mut H) {
ContentHash::content_hash(&self.value, state);
ContentHash::content_hash(&self.regex, state);
ContentHash::content_hash(&self.raw, state);
}
}
@ -55,10 +55,6 @@ impl<'a> ContentHash for RegExpPattern<'a> {
}
}
impl ContentHash for EmptyObject {
fn content_hash<H: Hasher>(&self, _: &mut H) {}
}
impl<'a> ContentHash for StringLiteral<'a> {
fn content_hash<H: Hasher>(&self, state: &mut H) {
ContentHash::content_hash(&self.value, state);

View file

@ -15,52 +15,31 @@ use crate::ast::ts::*;
impl Serialize for BooleanLiteral {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("type", "BooleanLiteral")?;
self.span.serialize(serde::__private::ser::FlatMapSerializer(&mut map))?;
map.serialize_entry("value", &self.value)?;
map.end()
crate::serialize::ESTreeLiteral::from(self).serialize(serializer)
}
}
impl Serialize for NullLiteral {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("type", "NullLiteral")?;
self.span.serialize(serde::__private::ser::FlatMapSerializer(&mut map))?;
map.end()
crate::serialize::ESTreeLiteral::from(self).serialize(serializer)
}
}
impl<'a> Serialize for NumericLiteral<'a> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("type", "NumericLiteral")?;
self.span.serialize(serde::__private::ser::FlatMapSerializer(&mut map))?;
map.serialize_entry("value", &self.value)?;
map.serialize_entry("raw", &self.raw)?;
map.end()
crate::serialize::ESTreeLiteral::from(self).serialize(serializer)
}
}
impl<'a> Serialize for BigIntLiteral<'a> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("type", "BigIntLiteral")?;
self.span.serialize(serde::__private::ser::FlatMapSerializer(&mut map))?;
map.serialize_entry("raw", &self.raw)?;
map.end()
crate::serialize::ESTreeLiteral::from(self).serialize(serializer)
}
}
impl<'a> Serialize for RegExpLiteral<'a> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("type", "RegExpLiteral")?;
self.span.serialize(serde::__private::ser::FlatMapSerializer(&mut map))?;
map.serialize_entry("value", &self.value)?;
map.serialize_entry("regex", &self.regex)?;
map.end()
crate::serialize::ESTreeLiteral::from(self).serialize(serializer)
}
}
@ -83,13 +62,6 @@ impl<'a> Serialize for RegExpPattern<'a> {
}
}
impl Serialize for EmptyObject {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.end()
}
}
impl<'a> Serialize for StringLiteral<'a> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;

View file

@ -1,16 +1,113 @@
use cow_utils::CowUtils;
use num_bigint::BigInt;
use num_traits::Num;
use oxc_allocator::Box;
use oxc_span::Span;
use oxc_syntax::number::BigintBase;
use serde::{
ser::{SerializeSeq, Serializer},
Serialize,
};
use crate::ast::{
BindingPatternKind, Directive, Elision, FormalParameter, FormalParameterKind, FormalParameters,
JSXElementName, JSXIdentifier, JSXMemberExpressionObject, Program, RegExpFlags, Statement,
StringLiteral, TSModuleBlock, TSTypeAnnotation,
BigIntLiteral, BindingPatternKind, BooleanLiteral, Directive, Elision, FormalParameter,
FormalParameterKind, FormalParameters, JSXElementName, JSXIdentifier,
JSXMemberExpressionObject, NullLiteral, NumericLiteral, Program, RegExpFlags, RegExpLiteral,
RegExpPattern, Statement, StringLiteral, TSModuleBlock, TSTypeAnnotation,
};
#[derive(Serialize)]
#[serde(tag = "type", rename = "Literal")]
pub struct ESTreeLiteral<'a, T> {
#[serde(flatten)]
span: Span,
value: T,
raw: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
bigint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
regex: Option<SerRegExpValue>,
}
impl<'a> From<&BooleanLiteral> for ESTreeLiteral<'a, bool> {
fn from(value: &BooleanLiteral) -> Self {
Self {
span: value.span,
value: value.value,
raw: if value.value { "true" } else { "false" },
bigint: None,
regex: None,
}
}
}
impl<'a> From<&NullLiteral> for ESTreeLiteral<'a, ()> {
fn from(value: &NullLiteral) -> Self {
Self { span: value.span, value: (), raw: "null", bigint: None, regex: None }
}
}
impl<'a> From<&'a NumericLiteral<'a>> for ESTreeLiteral<'a, f64> {
fn from(value: &'a NumericLiteral) -> Self {
Self { span: value.span, value: value.value, raw: value.raw, bigint: None, regex: None }
}
}
impl<'a> From<&'a BigIntLiteral<'a>> for ESTreeLiteral<'a, ()> {
fn from(value: &'a BigIntLiteral) -> Self {
let src = &value.raw.strip_suffix('n').unwrap().cow_replace('_', "");
let src = match value.base {
BigintBase::Decimal => src,
BigintBase::Binary | BigintBase::Octal | BigintBase::Hex => &src[2..],
};
let radix = match value.base {
BigintBase::Decimal => 10,
BigintBase::Binary => 2,
BigintBase::Octal => 8,
BigintBase::Hex => 16,
};
let bigint = BigInt::from_str_radix(src, radix).unwrap();
Self {
span: value.span,
// BigInts can't be serialized to JSON
value: (),
raw: value.raw.as_str(),
bigint: Some(bigint.to_string()),
regex: None,
}
}
}
#[derive(Serialize)]
pub struct SerRegExpValue {
pattern: String,
flags: String,
}
/// A placeholder for regexp literals that can't be serialized to JSON
#[derive(Serialize)]
#[allow(clippy::empty_structs_with_brackets)]
pub struct EmptyObject {}
impl<'a> From<&'a RegExpLiteral<'a>> for ESTreeLiteral<'a, Option<EmptyObject>> {
fn from(value: &'a RegExpLiteral) -> Self {
Self {
span: value.span,
raw: value.raw,
value: match &value.regex.pattern {
RegExpPattern::Pattern(_) => Some(EmptyObject {}),
_ => None,
},
bigint: None,
regex: Some(SerRegExpValue {
pattern: value.regex.pattern.to_string(),
flags: value.regex.flags.to_string(),
}),
}
}
}
pub struct EcmaFormatter;
/// Serialize f64 with `ryu_js`

View file

@ -350,6 +350,7 @@ impl<'a> ParserImpl<'a> {
let pattern_text = &self.source_text[pattern_start as usize..pattern_end as usize];
let flags_start = pattern_end + 1; // +1 to include right `/`
let flags_text = &self.source_text[flags_start as usize..self.cur_token().end as usize];
let raw = self.cur_src();
self.bump_any();
// Parse pattern if options is enabled and also flags are valid
let pattern = (self.options.parse_regular_expression && !flags_error)
@ -363,7 +364,7 @@ impl<'a> ParserImpl<'a> {
pat.map_or_else(|| RegExpPattern::Invalid(pattern_text), RegExpPattern::Pattern)
},
);
Ok(self.ast.reg_exp_literal(self.end_span(span), EmptyObject, RegExp { pattern, flags }))
Ok(self.ast.reg_exp_literal(self.end_span(span), RegExp { pattern, flags }, raw))
}
fn parse_regex_pattern(

View file

@ -2,29 +2,35 @@
// To edit this generated file you have to edit `tasks/ast_tools/src/generators/typescript.rs`
export interface BooleanLiteral extends Span {
type: 'BooleanLiteral';
type: 'Literal';
value: boolean;
raw: string;
}
export interface NullLiteral extends Span {
type: 'NullLiteral';
type: 'Literal';
value: null;
raw: 'null';
}
export interface NumericLiteral extends Span {
type: 'NumericLiteral';
type: 'Literal';
value: number;
raw: string;
}
export interface BigIntLiteral extends Span {
type: 'BigIntLiteral';
type: 'Literal';
raw: string;
value: null;
bigint: string;
}
export interface RegExpLiteral extends Span {
type: 'RegExpLiteral';
value: EmptyObject;
regex: RegExp;
type: 'Literal';
raw: string;
value: {} | null;
regex: { pattern: string; flags: string };
}
export interface RegExp {
@ -34,9 +40,6 @@ export interface RegExp {
export type RegExpPattern = string | string | Pattern;
export interface EmptyObject {
}
export interface StringLiteral extends Span {
type: 'StringLiteral';
value: string;

View file

@ -66,6 +66,13 @@ impl Derive for DeriveESTree {
}
fn serialize_struct(def: &StructDef, schema: &Schema) -> TokenStream {
if let Some(via) = &def.markers.estree.as_ref().and_then(|e| e.via.as_ref()) {
let via: TokenStream = via.parse().unwrap();
return quote! {
#via::from(self).serialize(serializer)
};
}
let ident = def.ident();
// If type_tag is Some, we serialize it manually. If None, either one of
// the fields is named r#type, or the struct does not need a "type" field.

View file

@ -101,17 +101,23 @@ fn typescript_struct(def: &StructDef, always_flatten_structs: &FxHashSet<TypeId>
let extends_union = extends.iter().any(|it| it.contains('|'));
let body = if let Some(extra_ts) = def.markers.estree.as_ref().and_then(|e| e.add_ts.as_ref()) {
format!("{{{fields}\n\t{extra_ts}\n}}")
} else {
format!("{{{fields}\n}}")
};
if extends_union {
let extends =
if extends.is_empty() { String::new() } else { format!(" & {}", extends.join(" & ")) };
format!("export type {ident} = ({{{fields}\n}}){extends};")
format!("export type {ident} = ({body}){extends};")
} else {
let extends = if extends.is_empty() {
String::new()
} else {
format!(" extends {}", extends.join(", "))
};
format!("export interface {ident}{extends} {{{fields}\n}}")
format!("export interface {ident}{extends} {body}")
}
}

View file

@ -1,4 +1,5 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use serde::Serialize;
use syn::{
ext::IdentExt,
@ -7,7 +8,7 @@ use syn::{
parse2,
punctuated::{self, Punctuated},
spanned::Spanned,
token, Attribute, Expr, Ident, LitStr, Meta, MetaNameValue, Token,
token, Attribute, Expr, Ident, LitStr, Meta, MetaNameValue, Path, Token,
};
use crate::util::NormalizeError;
@ -99,6 +100,8 @@ impl From<&Ident> for CloneInAttribute {
pub struct ESTreeStructAttribute {
pub tag_mode: Option<ESTreeStructTagMode>,
pub always_flatten: bool,
pub via: Option<String>,
pub add_ts: Option<String>,
}
#[derive(Debug, Serialize, PartialEq, Eq)]
@ -112,6 +115,8 @@ impl Parse for ESTreeStructAttribute {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let mut tag_mode = None;
let mut always_flatten = false;
let mut via = None;
let mut add_ts = None;
loop {
let is_type = input.peek(Token![type]);
@ -149,6 +154,16 @@ impl Parse for ESTreeStructAttribute {
"Duplicate tag mode in #[estree(...)]"
);
}
"via" => {
input.parse::<Token![=]>()?;
let value = input.parse::<Path>()?.to_token_stream().to_string();
assert!(via.replace(value).is_none(), "Duplicate estree(via)");
}
"add_ts" => {
input.parse::<Token![=]>()?;
let value = input.parse::<LitStr>()?.value();
assert!(add_ts.replace(value).is_none(), "Duplicate estree(add_ts)");
}
arg => panic!("Unsupported #[estree(...)] argument: {arg}"),
}
let comma = input.peek(Token![,]);
@ -158,7 +173,7 @@ impl Parse for ESTreeStructAttribute {
break;
}
}
Ok(Self { tag_mode, always_flatten })
Ok(Self { tag_mode, always_flatten, via, add_ts })
}
}