diff --git a/Cargo.lock b/Cargo.lock index 171b37d0f27e..77fa4460e8f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6084,6 +6084,7 @@ dependencies = [ "string_enum", "swc_atoms", "swc_common", + "swc_ecma_parser", "swc_visit", "unicode-id-start", ] diff --git a/crates/swc_ecma_ast/Cargo.toml b/crates/swc_ecma_ast/Cargo.toml index 09c9d80eaf9a..e6cda5590579 100644 --- a/crates/swc_ecma_ast/Cargo.toml +++ b/crates/swc_ecma_ast/Cargo.toml @@ -35,7 +35,7 @@ rkyv-impl = [ "swc_atoms/rkyv-impl", "swc_common/rkyv-impl", ] -serde-impl = ["serde"] +serde-impl = ["serde", "dep:serde_json"] shrink-to-fit = [ "dep:shrink-to-fit", "swc_atoms/shrink-to-fit", @@ -54,6 +54,7 @@ rancor = { workspace = true, optional = true } rkyv = { workspace = true, optional = true } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } +serde_json = { workspace = true, optional = true } shrink-to-fit = { workspace = true, optional = true } unicode-id-start = { workspace = true } @@ -66,3 +67,4 @@ cbor4ii = { workspace = true, features = ["use_std"], optional = true } [dev-dependencies] serde_json = { workspace = true } +swc_ecma_parser = { version = "38.0.0", path = "../swc_ecma_parser", features = ["typescript"] } diff --git a/crates/swc_ecma_ast/src/class.rs b/crates/swc_ecma_ast/src/class.rs index b3591a11ed43..41e62adea20f 100644 --- a/crates/swc_ecma_ast/src/class.rs +++ b/crates/swc_ecma_ast/src/class.rs @@ -21,6 +21,7 @@ use crate::{ pub struct Class { pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, #[cfg_attr(feature = "serde-impl", serde(default))] @@ -278,6 +279,7 @@ pub struct PrivateMethod { pub struct Constructor { pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, pub key: PropName, diff --git a/crates/swc_ecma_ast/src/decl.rs b/crates/swc_ecma_ast/src/decl.rs index 8e8ecfd40232..bba4daf8c6ea 100644 --- a/crates/swc_ecma_ast/src/decl.rs +++ b/crates/swc_ecma_ast/src/decl.rs @@ -153,6 +153,7 @@ impl Take for ClassDecl { pub struct VarDecl { pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, pub kind: VarDeclKind, diff --git a/crates/swc_ecma_ast/src/expr.rs b/crates/swc_ecma_ast/src/expr.rs index b73682011b51..d87186494fe7 100644 --- a/crates/swc_ecma_ast/src/expr.rs +++ b/crates/swc_ecma_ast/src/expr.rs @@ -997,6 +997,7 @@ impl Take for CondExpr { #[cfg_attr(feature = "shrink-to-fit", derive(shrink_to_fit::ShrinkToFit))] pub struct CallExpr { pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, pub callee: Callee, @@ -1026,6 +1027,7 @@ impl Take for CallExpr { pub struct NewExpr { pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, pub callee: Box, @@ -1079,6 +1081,7 @@ impl Take for SeqExpr { pub struct ArrowExpr { pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, pub params: Vec, @@ -1214,6 +1217,7 @@ impl Take for Tpl { pub struct TaggedTpl { pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, pub tag: Box, @@ -1748,6 +1752,7 @@ impl Default for OptChainBase { pub struct OptCall { pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, pub callee: Box, diff --git a/crates/swc_ecma_ast/src/function.rs b/crates/swc_ecma_ast/src/function.rs index 3d0747b3a00b..5869b6b2dab8 100644 --- a/crates/swc_ecma_ast/src/function.rs +++ b/crates/swc_ecma_ast/src/function.rs @@ -21,6 +21,7 @@ pub struct Function { pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, #[cfg_attr(feature = "serde-impl", serde(default))] diff --git a/crates/swc_ecma_ast/src/ident.rs b/crates/swc_ecma_ast/src/ident.rs index 38a68175f0a4..e017f5a3052d 100644 --- a/crates/swc_ecma_ast/src/ident.rs +++ b/crates/swc_ecma_ast/src/ident.rs @@ -184,6 +184,7 @@ pub struct Ident { #[cfg_attr(feature = "__rkyv", rkyv(omit_bounds))] pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] #[cfg_attr(feature = "__rkyv", rkyv(omit_bounds))] pub ctxt: SyntaxContext, diff --git a/crates/swc_ecma_ast/src/lib.rs b/crates/swc_ecma_ast/src/lib.rs index faed62d44fd3..24ac105c010f 100644 --- a/crates/swc_ecma_ast/src/lib.rs +++ b/crates/swc_ecma_ast/src/lib.rs @@ -89,6 +89,8 @@ mod module_decl; mod operators; mod pat; mod prop; +#[cfg(feature = "serde-impl")] +mod serde_impl; mod source_map; mod stmt; mod typescript; diff --git a/crates/swc_ecma_ast/src/module.rs b/crates/swc_ecma_ast/src/module.rs index 339f4c45f72f..207354b1ddd8 100644 --- a/crates/swc_ecma_ast/src/module.rs +++ b/crates/swc_ecma_ast/src/module.rs @@ -4,11 +4,58 @@ use swc_common::{ast_node, util::take::Take, EqIgnoreSpan, Span, DUMMY_SP}; use crate::{module_decl::ModuleDecl, stmt::Stmt}; -#[ast_node] -#[derive(Eq, Hash, Is, EqIgnoreSpan)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive( + ::swc_common::FromVariant, + ::swc_common::Spanned, + Debug, + PartialEq, + ::swc_common::DeserializeEnum, + Clone, + Eq, + Hash, + Is, + EqIgnoreSpan, +)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "shrink-to-fit", derive(shrink_to_fit::ShrinkToFit))] +#[cfg_attr(swc_ast_unknown, non_exhaustive)] +#[cfg_attr( + feature = "rkyv-impl", + derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize) +)] +#[cfg_attr( + feature = "rkyv-impl", + rkyv(deserialize_bounds(__D::Error: rkyv::rancor::Source)) +)] +#[cfg_attr(feature = "rkyv-impl", repr(u32))] +#[cfg_attr( + feature = "rkyv-impl", + rkyv( + serialize_bounds( + __S: rkyv::ser::Writer + rkyv::ser::Allocator, + __S::Error: rkyv::rancor::Source + ) + ) +)] +#[cfg_attr( + feature = "rkyv-impl", + rkyv(bytecheck(bounds( + __C: rkyv::validation::ArchiveContext, + __C::Error: rkyv::rancor::Source + ))) +)] +#[cfg_attr( + feature = "encoding-impl", + derive(::swc_common::Encode, ::swc_common::Decode) +)] pub enum Program { + #[cfg(all(swc_ast_unknown, feature = "encoding-impl"))] + #[from_variant(ignore)] + #[span(unknown)] + #[encoding(unknown)] + Unknown(u32, swc_common::unknown::Unknown), + #[tag("Module")] Module(Module), #[tag("Script")] diff --git a/crates/swc_ecma_ast/src/serde_impl.rs b/crates/swc_ecma_ast/src/serde_impl.rs new file mode 100644 index 000000000000..398c70400b38 --- /dev/null +++ b/crates/swc_ecma_ast/src/serde_impl.rs @@ -0,0 +1,458 @@ +use serde::{Serialize, Serializer}; +use serde_json::{Map, Value}; + +use crate::Program; + +/// Serialize `swc_ecma_ast::Program` as a typescript-eslint-oriented AST. +/// +/// This intentionally only changes serialization. Deserialization continues to +/// use the existing SWC-facing representation. +#[derive(Serialize)] +#[serde(transparent)] +pub(crate) struct ProgramSerde(Value); + +impl Serialize for Program { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + ProgramSerde::from(self.clone()).serialize(serializer) + } +} + +impl From for ProgramSerde { + fn from(program: Program) -> Self { + let normalized = match program { + Program::Module(module) => normalize_program( + serde_json::to_value(module).expect("failed to serialize module"), + "module", + ), + Program::Script(script) => normalize_program( + serde_json::to_value(script).expect("failed to serialize script"), + "script", + ), + #[cfg(all(swc_ast_unknown, feature = "encoding-impl"))] + Program::Unknown(..) => unreachable!("unknown program nodes cannot be serialized"), + }; + + ProgramSerde(normalized) + } +} + +fn normalize_program(value: Value, source_type: &'static str) -> Value { + let mut program = as_object(normalize_value(value)); + program.insert("type".into(), string("Program")); + program.insert("sourceType".into(), string(source_type)); + + if matches!(program.get("interpreter"), Some(Value::Null)) { + program.remove("interpreter"); + } + + Value::Object(program) +} + +fn normalize_value(value: Value) -> Value { + match value { + Value::Array(values) => Value::Array(values.into_iter().map(normalize_value).collect()), + Value::Object(object) => normalize_object(object), + other => other, + } +} + +fn normalize_object(mut object: Map) -> Value { + object.remove("span"); + object.remove("ctxt"); + + let ty = object + .get("type") + .and_then(Value::as_str) + .map(ToOwned::to_owned); + + for value in object.values_mut() { + *value = normalize_value(std::mem::take(value)); + } + + apply_common_field_renames(&mut object); + + if let Some(ty) = ty { + object.insert("type".into(), string(&normalize_type_name(&ty))); + + return match ty.as_str() { + "PrivateName" => normalize_private_identifier(object), + "MemberExpression" => normalize_member_expression(object), + "SuperPropExpression" => normalize_super_prop_expression(object), + "OptionalChainingExpression" => normalize_opt_chain(object), + "ClassProperty" | "PrivateProperty" => normalize_property_definition(object), + "ClassMethod" | "PrivateMethod" | "Constructor" => normalize_method_definition(object), + "Parameter" => normalize_param(object), + "TsParameterProperty" => normalize_ts_parameter_property(object), + "TsPropertySignature" | "TsMethodSignature" => normalize_ts_type_keyed_member(object), + "TsNamespaceDeclaration" => normalize_ts_namespace_decl(object), + _ => Value::Object(object), + }; + } + + Value::Object(object) +} + +fn apply_common_field_renames(object: &mut Map) { + rename_key(object, "identifier", "id"); + rename_key(object, "typeParams", "typeParameters"); + rename_key(object, "superTypeParams", "superTypeArguments"); + rename_key(object, "isAbstract", "abstract"); + rename_key(object, "isStatic", "static"); + rename_key(object, "isOptional", "optional"); + rename_key(object, "isOverride", "override"); +} + +fn normalize_type_name(ty: &str) -> String { + match ty { + "ParenthesisExpression" => "ParenthesizedExpression".into(), + "TsNamespaceDeclaration" => "TSModuleDeclaration".into(), + _ if ty.starts_with("Ts") => format!("TS{}", &ty[2..]), + _ => ty.into(), + } +} + +fn normalize_private_identifier(mut object: Map) -> Value { + let name = object + .remove("value") + .or_else(|| object.remove("id")) + .or_else(|| object.remove("name")) + .and_then(extract_identifier_name) + .unwrap_or_default(); + + let mut private_identifier = Map::new(); + private_identifier.insert("type".into(), string("PrivateIdentifier")); + private_identifier.insert("name".into(), Value::String(name)); + + Value::Object(private_identifier) +} + +fn normalize_member_expression(mut object: Map) -> Value { + let property = object.remove("property").unwrap_or(Value::Null); + let (property, computed) = unpack_computed_key(property); + + object.insert("property".into(), property); + object.insert("computed".into(), Value::Bool(computed)); + + Value::Object(object) +} + +fn normalize_super_prop_expression(mut object: Map) -> Value { + let property = object + .remove("property") + .unwrap_or_else(|| object.remove("prop").unwrap_or(Value::Null)); + let (property, computed) = unpack_computed_key(property); + + let mut member_expr = Map::new(); + member_expr.insert("type".into(), string("MemberExpression")); + member_expr.insert( + "object".into(), + object + .remove("obj") + .unwrap_or(Value::Object(Map::from_iter([( + "type".into(), + string("Super"), + )]))), + ); + member_expr.insert("property".into(), property); + member_expr.insert("computed".into(), Value::Bool(computed)); + + Value::Object(member_expr) +} + +fn normalize_opt_chain(mut object: Map) -> Value { + let optional = take_bool(&mut object, "optional"); + let mut expression = object.remove("base").unwrap_or(Value::Null); + + if let Value::Object(ref mut base) = expression { + base.insert("optional".into(), Value::Bool(optional)); + } + + let mut chain_expr = Map::new(); + chain_expr.insert("type".into(), string("ChainExpression")); + chain_expr.insert("expression".into(), expression); + + Value::Object(chain_expr) +} + +fn normalize_property_definition(mut object: Map) -> Value { + let key = object.remove("key").unwrap_or(Value::Null); + let (key, computed) = unpack_computed_key(key); + + let mut property_definition = Map::new(); + property_definition.insert("type".into(), string("PropertyDefinition")); + property_definition.insert("key".into(), key); + property_definition.insert("computed".into(), Value::Bool(computed)); + property_definition.insert( + "static".into(), + Value::Bool(take_bool(&mut object, "static")), + ); + + move_value(&mut object, &mut property_definition, "value"); + move_value(&mut object, &mut property_definition, "typeAnnotation"); + move_non_empty_array(&mut object, &mut property_definition, "decorators"); + move_optional_value(&mut object, &mut property_definition, "accessibility"); + move_true_bool(&mut object, &mut property_definition, "abstract"); + move_true_bool(&mut object, &mut property_definition, "optional"); + move_true_bool(&mut object, &mut property_definition, "override"); + move_true_bool(&mut object, &mut property_definition, "readonly"); + move_true_bool(&mut object, &mut property_definition, "declare"); + move_true_bool(&mut object, &mut property_definition, "definite"); + + Value::Object(property_definition) +} + +fn normalize_method_definition(mut object: Map) -> Value { + let original_type = take_type(&mut object); + + let key = object.remove("key").unwrap_or(Value::Null); + let (key, computed) = unpack_computed_key(key); + + let kind = match original_type.as_deref() { + Some("Constructor") => "constructor".to_string(), + _ => match object.get("kind").and_then(Value::as_str) { + Some("getter") => "get".to_string(), + Some("setter") => "set".to_string(), + Some("method") | None => "method".to_string(), + Some(other) => other.to_string(), + }, + }; + + let value = if let Some(function) = object.remove("function") { + into_function_expression(function) + } else { + into_function_expression(Value::Object(Map::from_iter([ + ( + "params".into(), + object.remove("params").unwrap_or_else(empty_array), + ), + ("body".into(), object.remove("body").unwrap_or(Value::Null)), + ("generator".into(), Value::Bool(false)), + ("async".into(), Value::Bool(false)), + ]))) + }; + + let mut method_definition = Map::new(); + method_definition.insert("type".into(), string("MethodDefinition")); + method_definition.insert("key".into(), key); + method_definition.insert("computed".into(), Value::Bool(computed)); + method_definition.insert("kind".into(), string(&kind)); + method_definition.insert( + "static".into(), + Value::Bool(take_bool(&mut object, "static")), + ); + method_definition.insert("value".into(), value); + + move_non_empty_array(&mut object, &mut method_definition, "decorators"); + move_optional_value(&mut object, &mut method_definition, "accessibility"); + move_true_bool(&mut object, &mut method_definition, "abstract"); + move_true_bool(&mut object, &mut method_definition, "optional"); + move_true_bool(&mut object, &mut method_definition, "override"); + + Value::Object(method_definition) +} + +fn into_function_expression(value: Value) -> Value { + let mut function = as_object(value); + let decorators = function.remove("decorators"); + + let mut function_expr = Map::new(); + function_expr.insert("type".into(), string("FunctionExpression")); + function_expr.insert("id".into(), Value::Null); + function_expr.insert( + "params".into(), + normalize_params(function.remove("params").unwrap_or_else(empty_array)), + ); + function_expr.insert( + "body".into(), + function.remove("body").unwrap_or(Value::Null), + ); + function_expr.insert( + "generator".into(), + Value::Bool(take_bool(&mut function, "generator")), + ); + function_expr.insert( + "async".into(), + Value::Bool(take_bool(&mut function, "async")), + ); + + move_optional_value(&mut function, &mut function_expr, "typeParameters"); + move_optional_value(&mut function, &mut function_expr, "returnType"); + + if let Some(Value::Array(decorators)) = decorators { + if !decorators.is_empty() { + function_expr.insert("decorators".into(), Value::Array(decorators)); + } + } + + Value::Object(function_expr) +} + +fn normalize_params(value: Value) -> Value { + match value { + Value::Array(params) => { + Value::Array(params.into_iter().map(normalize_param_value).collect()) + } + other => other, + } +} + +fn normalize_param_value(value: Value) -> Value { + match value { + Value::Object(object) + if matches!( + object.get("type").and_then(Value::as_str), + Some("Parameter") + ) => + { + normalize_param(object) + } + other => other, + } +} + +fn normalize_param(mut object: Map) -> Value { + let decorators = object.remove("decorators"); + let mut pattern = object.remove("pat").unwrap_or(Value::Null); + + if let Some(Value::Array(decorators)) = decorators { + if !decorators.is_empty() { + if let Value::Object(ref mut pattern_object) = pattern { + pattern_object.insert("decorators".into(), Value::Array(decorators)); + } + } + } + + pattern +} + +fn normalize_ts_parameter_property(mut object: Map) -> Value { + let mut ts_param_prop = Map::new(); + ts_param_prop.insert("type".into(), string("TSParameterProperty")); + ts_param_prop.insert( + "parameter".into(), + object.remove("param").unwrap_or(Value::Null), + ); + + move_non_empty_array(&mut object, &mut ts_param_prop, "decorators"); + move_optional_value(&mut object, &mut ts_param_prop, "accessibility"); + move_true_bool(&mut object, &mut ts_param_prop, "readonly"); + move_true_bool(&mut object, &mut ts_param_prop, "override"); + + Value::Object(ts_param_prop) +} + +fn normalize_ts_type_keyed_member(mut object: Map) -> Value { + let key = object.remove("key").unwrap_or(Value::Null); + let (key, computed) = unpack_computed_key(key); + object.insert("key".into(), key); + object.insert( + "computed".into(), + Value::Bool( + if matches!(object.get("computed"), Some(Value::Bool(true))) { + true + } else { + computed + }, + ), + ); + + Value::Object(object) +} + +fn normalize_ts_namespace_decl(mut object: Map) -> Value { + let mut module_decl = Map::new(); + module_decl.insert("type".into(), string("TSModuleDeclaration")); + move_value(&mut object, &mut module_decl, "id"); + move_value(&mut object, &mut module_decl, "body"); + move_true_bool(&mut object, &mut module_decl, "declare"); + move_true_bool(&mut object, &mut module_decl, "global"); + + Value::Object(module_decl) +} + +fn unpack_computed_key(value: Value) -> (Value, bool) { + match value { + Value::Object(mut object) + if matches!(object.get("type").and_then(Value::as_str), Some("Computed")) => + { + (object.remove("expression").unwrap_or(Value::Null), true) + } + other => (other, false), + } +} + +fn extract_identifier_name(value: Value) -> Option { + match value { + Value::Object(mut object) => object + .remove("value") + .or_else(|| object.remove("name")) + .and_then(|value| value.as_str().map(ToOwned::to_owned)), + Value::String(value) => Some(value), + _ => None, + } +} + +fn rename_key(object: &mut Map, from: &str, to: &str) { + if let Some(value) = object.remove(from) { + object.insert(to.into(), value); + } +} + +fn move_value(from: &mut Map, to: &mut Map, key: &str) { + if let Some(value) = from.remove(key) { + to.insert(key.into(), value); + } +} + +fn move_optional_value(from: &mut Map, to: &mut Map, key: &str) { + if let Some(value) = from.remove(key) { + if !value.is_null() { + to.insert(key.into(), value); + } + } +} + +fn move_non_empty_array(from: &mut Map, to: &mut Map, key: &str) { + if let Some(Value::Array(values)) = from.remove(key) { + if !values.is_empty() { + to.insert(key.into(), Value::Array(values)); + } + } +} + +fn move_true_bool(from: &mut Map, to: &mut Map, key: &str) { + if take_bool(from, key) { + to.insert(key.into(), Value::Bool(true)); + } +} + +fn take_bool(object: &mut Map, key: &str) -> bool { + object + .remove(key) + .and_then(|value| value.as_bool()) + .unwrap_or(false) +} + +fn take_type(object: &mut Map) -> Option { + object + .remove("type") + .and_then(|value| value.as_str().map(ToOwned::to_owned)) +} + +fn as_object(value: Value) -> Map { + match value { + Value::Object(object) => object, + _ => Map::new(), + } +} + +fn string(value: &str) -> Value { + Value::String(value.into()) +} + +fn empty_array() -> Value { + Value::Array(Vec::new()) +} diff --git a/crates/swc_ecma_ast/src/stmt.rs b/crates/swc_ecma_ast/src/stmt.rs index 328308aec6d2..00b66df21995 100644 --- a/crates/swc_ecma_ast/src/stmt.rs +++ b/crates/swc_ecma_ast/src/stmt.rs @@ -17,6 +17,7 @@ pub struct BlockStmt { /// Span including the braces. pub span: Span, + #[cfg_attr(feature = "serde-impl", serde(default))] pub ctxt: SyntaxContext, pub stmts: Vec, diff --git a/crates/swc_ecma_ast/tests/serde.rs b/crates/swc_ecma_ast/tests/serde.rs new file mode 100644 index 000000000000..58ae96497261 --- /dev/null +++ b/crates/swc_ecma_ast/tests/serde.rs @@ -0,0 +1,387 @@ +#![cfg(feature = "serde-impl")] + +use serde_json::{Map, Value}; +use swc_common::{sync::Lrc, FileName, SourceMap}; +use swc_ecma_ast::{EsVersion, Program}; +use swc_ecma_parser::{parse_file_as_program, parse_file_as_script, Syntax, TsSyntax}; + +fn serialize_program(program: &Program) -> Value { + serde_json::to_value(program).expect("failed to serialize program") +} + +fn parse_program(src: &str, syntax: Syntax) -> Program { + let cm: Lrc = Default::default(); + let fm = cm.new_source_file( + FileName::Custom("serde-normalization.ts".into()).into(), + src.to_string(), + ); + let mut errors = Vec::new(); + let program = parse_file_as_program(&fm, syntax, EsVersion::EsNext, None, &mut errors) + .expect("failed to parse program"); + + assert!( + errors.is_empty(), + "unexpected recovered parser errors: {errors:#?}" + ); + + program +} + +fn parse_script(src: &str) -> Program { + let cm: Lrc = Default::default(); + let fm = cm.new_source_file( + FileName::Custom("serde-normalization.js".into()).into(), + src.to_string(), + ); + let mut errors = Vec::new(); + let script = parse_file_as_script( + &fm, + Syntax::Es(Default::default()), + EsVersion::EsNext, + None, + &mut errors, + ) + .expect("failed to parse script"); + + assert!( + errors.is_empty(), + "unexpected recovered parser errors: {errors:#?}" + ); + + Program::Script(script) +} + +fn ts_syntax() -> Syntax { + Syntax::Typescript(TsSyntax { + decorators: true, + ..Default::default() + }) +} + +fn visit_objects<'a>(value: &'a Value, visitor: &mut impl FnMut(&'a Map)) { + match value { + Value::Object(object) => { + visitor(object); + + for child in object.values() { + visit_objects(child, visitor); + } + } + Value::Array(values) => { + for value in values { + visit_objects(value, visitor); + } + } + _ => {} + } +} + +fn nodes_of_type<'a>(value: &'a Value, ty: &str) -> Vec<&'a Map> { + let mut nodes = Vec::new(); + + visit_objects(value, &mut |object| { + if object.get("type").and_then(Value::as_str) == Some(ty) { + nodes.push(object); + } + }); + + nodes +} + +fn assert_no_internal_fields(value: &Value) { + visit_objects(value, &mut |object| { + assert!( + !object.contains_key("span"), + "unexpected `span` in node: {object:#?}" + ); + assert!( + !object.contains_key("ctxt"), + "unexpected `ctxt` in node: {object:#?}" + ); + }); +} + +fn assert_normalized_type_names(value: &Value) { + visit_objects(value, &mut |object| { + if let Some(ty) = object.get("type").and_then(Value::as_str) { + assert!( + !ty.starts_with("Ts"), + "found non-normalized type name `{ty}` in node: {object:#?}" + ); + } + }); +} + +fn node_key_name(node: &Map) -> Option<&str> { + match node.get("key") { + Some(Value::Object(key)) => key + .get("name") + .or_else(|| key.get("value")) + .and_then(Value::as_str), + _ => None, + } +} + +fn property_definition_named<'a>(value: &'a Value, name: &str) -> &'a Map { + nodes_of_type(value, "PropertyDefinition") + .into_iter() + .find(|node| node_key_name(node) == Some(name)) + .unwrap_or_else(|| panic!("failed to find PropertyDefinition for `{name}`")) +} + +fn property_definition_with<'a>( + value: &'a Value, + name: &str, + key: &str, + expected: bool, +) -> &'a Map { + nodes_of_type(value, "PropertyDefinition") + .into_iter() + .find(|node| { + node_key_name(node) == Some(name) + && node.get(key).and_then(Value::as_bool) == Some(expected) + }) + .unwrap_or_else(|| panic!("failed to find PropertyDefinition `{name}` with `{key}`")) +} + +fn node_with_key_name<'a>(value: &'a Value, ty: &str, name: &str) -> &'a Map { + nodes_of_type(value, ty) + .into_iter() + .find(|node| node_key_name(node) == Some(name)) + .unwrap_or_else(|| panic!("failed to find `{ty}` node with key `{name}`")) +} + +fn find_super_member<'a>( + value: &'a Value, + property_name: &str, + computed: bool, +) -> &'a Map { + nodes_of_type(value, "MemberExpression") + .into_iter() + .find(|node| { + node.get("object") + .and_then(Value::as_object) + .and_then(|object| object.get("type")) + .and_then(Value::as_str) + == Some("Super") + && node.get("computed").and_then(Value::as_bool) == Some(computed) + && node + .get("property") + .and_then(Value::as_object) + .and_then(|property| property.get("name").or_else(|| property.get("value"))) + .and_then(Value::as_str) + == Some(property_name) + }) + .unwrap_or_else(|| { + panic!("failed to find super member expression `{property_name}` (computed={computed})") + }) +} + +fn assert_has_type(value: &Value, ty: &str) { + assert!( + !nodes_of_type(value, ty).is_empty(), + "failed to find node of type `{ty}`" + ); +} + +fn has_optional_chain_element(value: &Value) -> bool { + match value { + Value::Object(object) => { + if object.get("optional").and_then(Value::as_bool) == Some(true) { + return true; + } + + object.values().any(has_optional_chain_element) + } + Value::Array(values) => values.iter().any(has_optional_chain_element), + _ => false, + } +} + +#[test] +fn program_root_is_typescript_estree_like() { + let script = serialize_program(&parse_script("const value = 1;")); + assert_eq!(script["type"], "Program"); + assert_eq!(script["sourceType"], "script"); + assert!(script.get("interpreter").is_none()); + assert_no_internal_fields(&script); + assert_normalized_type_names(&script); + + let module = serialize_program(&parse_program( + "export const value = 1;", + Syntax::Es(Default::default()), + )); + assert_eq!(module["type"], "Program"); + assert_eq!(module["sourceType"], "module"); + assert!(module.get("interpreter").is_none()); + assert_no_internal_fields(&module); + assert_normalized_type_names(&module); +} + +#[test] +fn representative_nodes_serialize_with_typescript_estree_shapes() { + let json = serialize_program(&parse_program( + r#" + export class Base { + other!: number; + foo() {} + } + + export class Example extends Base { + declare declared: string; + readonly definite!: number; + override other?: number; + #secret?: string; + + constructor(public readonly value: string, protected override member: number) { + super(); + super.foo; + super["bar"]; + + const chain = this.#secret?.toString(); + const parenthesized = (this.value); + const asExpr = this.value as string; + const assertion = this.value; + const nonNull = this.#secret!; + const satisfiesExpr = this.value satisfies string; + } + } + + export function takes(optional?: string) { + return optional; + } + + interface Contract { + readonly field?: string; + method?(arg: T): T; + readonly [key: string]: number; + } + + type Alias = string; + + declare namespace Outer.Inner { + export type Nested = number; + } + "#, + ts_syntax(), + )); + + assert_no_internal_fields(&json); + assert_normalized_type_names(&json); + + assert_has_type(&json, "ParenthesizedExpression"); + assert_has_type(&json, "ChainExpression"); + assert_has_type(&json, "TSAsExpression"); + assert_has_type(&json, "TSTypeAssertion"); + assert_has_type(&json, "TSNonNullExpression"); + assert_has_type(&json, "TSSatisfiesExpression"); + assert_has_type(&json, "TSPropertySignature"); + assert_has_type(&json, "TSMethodSignature"); + assert_has_type(&json, "TSIndexSignature"); + assert_has_type(&json, "TSInterfaceDeclaration"); + assert_has_type(&json, "TSTypeAliasDeclaration"); + assert_has_type(&json, "TSModuleDeclaration"); + assert_has_type(&json, "PrivateIdentifier"); + + let chain = nodes_of_type(&json, "ChainExpression") + .into_iter() + .next() + .expect("failed to find ChainExpression"); + let chain_expression = chain["expression"] + .as_object() + .expect("chain expression should be an object"); + assert_eq!( + chain_expression.get("type").and_then(Value::as_str), + Some("CallExpression") + ); + assert!( + has_optional_chain_element(&chain["expression"]), + "expected a chain element with `optional: true`: {chain:#?}" + ); + + let super_dot = find_super_member(&json, "foo", false); + assert_eq!(super_dot["object"]["type"], "Super"); + assert_eq!(super_dot["property"]["type"], "Identifier"); + assert_eq!(super_dot["property"]["value"], "foo"); + + let super_computed = find_super_member(&json, "bar", true); + assert_eq!(super_computed["object"]["type"], "Super"); + assert_eq!(super_computed["property"]["type"], "StringLiteral"); + assert_eq!(super_computed["property"]["value"], "bar"); + + let declared = property_definition_named(&json, "declared"); + assert_eq!(declared["declare"], true); + + let definite = property_definition_named(&json, "definite"); + assert_eq!(definite["readonly"], true); + assert_eq!(definite["definite"], true); + + let override_prop = property_definition_with(&json, "other", "override", true); + assert_eq!(override_prop["optional"], true); + + let private_prop = property_definition_named(&json, "secret"); + assert_eq!(private_prop["key"]["type"], "PrivateIdentifier"); + assert_eq!(private_prop["key"]["name"], "secret"); + + let constructor = nodes_of_type(&json, "MethodDefinition") + .into_iter() + .find(|node| node.get("kind").and_then(Value::as_str) == Some("constructor")) + .expect("failed to find constructor MethodDefinition"); + let constructor_value = constructor["value"] + .as_object() + .expect("constructor value should be a function expression"); + let params = constructor_value["params"] + .as_array() + .expect("constructor params should be an array"); + + assert!( + params + .iter() + .all(|param| param.get("type").and_then(Value::as_str) != Some("Parameter")), + "constructor params should not expose `Parameter` wrappers" + ); + assert!( + params + .iter() + .any(|param| param.get("type").and_then(Value::as_str) == Some("TSParameterProperty")), + "constructor params should include TSParameterProperty nodes" + ); + assert!( + params.iter().any(|param| { + param.get("type").and_then(Value::as_str) == Some("TSParameterProperty") + && param.get("readonly").and_then(Value::as_bool) == Some(true) + }), + "expected a readonly TSParameterProperty" + ); + assert!( + params.iter().any(|param| { + param.get("type").and_then(Value::as_str) == Some("TSParameterProperty") + && param.get("override").and_then(Value::as_bool) == Some(true) + }), + "expected an override TSParameterProperty" + ); + + let ts_property_signature = node_with_key_name(&json, "TSPropertySignature", "field"); + assert_eq!(ts_property_signature["readonly"], true); + assert_eq!(ts_property_signature["optional"], true); + + let ts_method_signature = node_with_key_name(&json, "TSMethodSignature", "method"); + assert_eq!(ts_method_signature["optional"], true); + assert!(ts_method_signature.get("typeParameters").is_some()); + + let ts_index_signature = nodes_of_type(&json, "TSIndexSignature") + .into_iter() + .next() + .expect("failed to find TSIndexSignature"); + assert_eq!(ts_index_signature["readonly"], true); + + let optional_identifier = nodes_of_type(&json, "Identifier") + .into_iter() + .find(|node| { + node.get("value").and_then(Value::as_str) == Some("optional") + && node.get("optional").and_then(Value::as_bool) == Some(true) + }) + .expect("failed to find optional Identifier"); + assert_eq!(optional_identifier["value"], "optional"); + assert!(optional_identifier.get("ctxt").is_none()); +}