Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/silent-cameras-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
swc_core: patch
swc_typescript: patch
---

fix(ts/fast-dts): Correctly emit Symbol-keyed accessors in declarations
47 changes: 11 additions & 36 deletions crates/swc_typescript/src/fast_dts/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use swc_ecma_ast::{

use super::{
type_ann,
util::ast_ext::{ExprExit, PatExt, PropNameExit},
util::ast_ext::{ExprExit, PatExt, PropNameExit, StaticProp},
FastDts,
};

Expand Down Expand Up @@ -155,10 +155,10 @@ impl FastDts {
} else {
method.function.params.truncate(1);
let param = method.function.params.first_mut().unwrap();
let static_name = method.key.static_name();
let static_prop = method.key.static_prop(self.unresolved_mark);

if let Some(type_ann) = static_name
.and_then(|name| setter_getter_annotations.get(name.as_ref()))
if let Some(type_ann) = static_prop
.and_then(|prop| setter_getter_annotations.get(&prop))
{
param.pat.set_type_ann(Some(type_ann.clone()));
}
Expand All @@ -179,8 +179,8 @@ impl FastDts {
if method.function.return_type.is_none() {
method.function.return_type = method
.key
.static_name()
.and_then(|name| setter_getter_annotations.get(name.as_ref()))
.static_prop(self.unresolved_mark)
.and_then(|prop| setter_getter_annotations.get(&prop))
.cloned();
}
if method.function.return_type.is_none() {
Expand Down Expand Up @@ -460,7 +460,7 @@ impl FastDts {
pub(crate) fn collect_getter_or_setter_annotations(
&mut self,
class: &Class,
) -> FxHashMap<String, Box<TsTypeAnn>> {
) -> FxHashMap<StaticProp, Box<TsTypeAnn>> {
let mut annotations = FxHashMap::default();
for member in &class.body {
let ClassMember::Method(method) = member else {
Expand All @@ -478,7 +478,7 @@ impl FastDts {
continue;
}

let Some(static_name) = method.key.static_name().map(|name| name.to_string()) else {
let Some(static_prop) = method.key.static_prop(self.unresolved_mark) else {
continue;
};

Expand All @@ -490,7 +490,7 @@ impl FastDts {
.clone()
.or_else(|| self.infer_function_return_type(&method.function))
{
annotations.insert(static_name, type_ann);
annotations.insert(static_prop, type_ann);
}
}
MethodKind::Setter => {
Expand All @@ -499,7 +499,7 @@ impl FastDts {
};

if let Some(type_ann) = first_param.pat.get_type_ann() {
annotations.insert(static_name, type_ann.clone());
annotations.insert(static_prop, type_ann.clone());
}
}
_ => continue,
Expand All @@ -522,31 +522,6 @@ impl FastDts {
}

pub(crate) fn is_global_symbol_object(&self, expr: &Expr) -> bool {
let Some(obj) = (match expr {
Expr::Member(member) => Some(&member.obj),
Expr::OptChain(opt_chain) => opt_chain.base.as_member().map(|member| &member.obj),
_ => None,
}) else {
return false;
};

// https://github.com/microsoft/TypeScript/blob/cbac1ddfc73ca3b9d8741c1b51b74663a0f24695/src/compiler/transformers/declarations.ts#L1011
if let Some(ident) = obj.as_ident() {
// Exactly `Symbol.something` and `Symbol` either does not resolve
// or definitely resolves to the global Symbol
return ident.sym.as_str() == "Symbol" && ident.ctxt.has_mark(self.unresolved_mark);
}

if let Some(member_expr) = obj.as_member() {
// Exactly `globalThis.Symbol.something` and `globalThis` resolves
// to the global `globalThis`
if let Some(ident) = member_expr.obj.as_ident() {
return ident.sym.as_str() == "globalThis"
&& ident.ctxt.has_mark(self.unresolved_mark)
&& member_expr.prop.is_ident_with("Symbol");
}
}

false
expr.get_global_symbol_prop(self.unresolved_mark).is_some()
}
}
46 changes: 15 additions & 31 deletions crates/swc_typescript/src/fast_dts/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::{
inferrer::ReturnTypeInferrer,
type_ann,
util::{
ast_ext::{ExprExit, PatExt},
ast_ext::{ExprExit, PatExt, StaticProp},
types::{ts_keyword_type, ts_lit_type},
},
FastDts,
Expand Down Expand Up @@ -191,30 +191,16 @@ impl FastDts {

let mut has_setter = false;

if let Some(static_name) = getter.key.static_name() {
if has_seen.contains(&static_name) {
if let Some(static_prop) = getter.key.static_prop(self.unresolved_mark) {
if has_seen.contains(&static_prop) {
continue;
}
has_setter = seen_setter.contains(static_name.as_ref());
if let Some(type_ann) =
setter_getter_annotations.get(static_name.as_ref())
{
has_setter = seen_setter.contains(&static_prop);
if let Some(type_ann) = setter_getter_annotations.get(&static_prop) {
getter_type_ann = Some(type_ann.clone());
}

has_seen.insert(static_name);
}

// [TODO]: check cases not handled by
// collect_object_getter_or_setter_annotations
if getter_type_ann.is_none() {
getter_type_ann = getter.type_ann.clone().or_else(|| {
getter
.body
.as_ref()
.and_then(|body| ReturnTypeInferrer::infer(self, &body.stmts))
.map(type_ann)
});
has_seen.insert(static_prop);
}

if getter_type_ann.is_none() {
Expand All @@ -238,17 +224,15 @@ impl FastDts {

let mut setter_type_ann = None;

if let Some(static_name) = setter.key.static_name() {
if has_seen.contains(&static_name) {
if let Some(static_prop) = setter.key.static_prop(self.unresolved_mark) {
if has_seen.contains(&static_prop) {
continue;
}
if let Some(type_ann) =
setter_getter_annotations.get(static_name.as_ref())
{
if let Some(type_ann) = setter_getter_annotations.get(&static_prop) {
setter_type_ann = Some(type_ann.clone());
}

has_seen.insert(static_name);
has_seen.insert(static_prop);
}

if setter_type_ann.is_none() {
Expand Down Expand Up @@ -434,7 +418,7 @@ impl FastDts {
pub(crate) fn collect_object_getter_or_setter_annotations(
&mut self,
object: &ObjectLit,
) -> (FxHashMap<String, Box<TsTypeAnn>>, FxHashSet<String>) {
) -> (FxHashMap<StaticProp, Box<TsTypeAnn>>, FxHashSet<StaticProp>) {
let mut annotations = FxHashMap::default();
let mut seen_setter = FxHashSet::default();

Expand All @@ -443,7 +427,7 @@ impl FastDts {
continue;
};

let Some(static_name) = prop.static_name().map(|name| name.to_string()) else {
let Some(static_prop) = prop.static_prop(self.unresolved_mark) else {
continue;
};

Expand All @@ -457,14 +441,14 @@ impl FastDts {
.clone()
.or_else(|| ReturnTypeInferrer::infer(self, &body.stmts).map(type_ann))
{
annotations.insert(static_name, type_ann);
annotations.insert(static_prop, type_ann);
}
}
Prop::Setter(setter) => {
if let Some(type_ann) = setter.param.get_type_ann().clone() {
annotations.insert(static_name.clone(), type_ann);
annotations.insert(static_prop.clone(), type_ann);
}
seen_setter.insert(static_name);
seen_setter.insert(static_prop);
}
_ => {}
}
Expand Down
123 changes: 106 additions & 17 deletions crates/swc_typescript/src/fast_dts/util/ast_ext.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::borrow::Cow;

use swc_atoms::Atom;
use swc_common::Mark;
use swc_ecma_ast::{
BindingIdent, Expr, Ident, Lit, MemberProp, ObjectPatProp, Pat, Prop, PropName, TsTypeAnn,
BindingIdent, ComputedPropName, Expr, Ident, Lit, MemberProp, ObjectPatProp, Pat, Prop,
PropName, TsTypeAnn,
};

pub trait ExprExit {
fn get_root_ident(&self) -> Option<&Ident>;
fn get_global_symbol_prop(&self, unresolved_mark: Mark) -> Option<&MemberProp>;
}

impl ExprExit for Expr {
Expand All @@ -21,6 +24,43 @@ impl ExprExit for Expr {
_ => None,
}
}

fn get_global_symbol_prop(&self, unresolved_mark: Mark) -> Option<&MemberProp> {
let (obj, prop) = (match self {
Expr::Member(member) => Some((&member.obj, &member.prop)),
Expr::OptChain(opt_chain) => opt_chain
.base
.as_member()
.map(|member| (&member.obj, &member.prop)),
_ => None,
})?;

// https://github.com/microsoft/TypeScript/blob/cbac1ddfc73ca3b9d8741c1b51b74663a0f24695/src/compiler/transformers/declarations.ts#L1011
if let Some(ident) = obj.as_ident() {
// Exactly `Symbol.something` and `Symbol` either does not resolve
// or definitely resolves to the global Symbol
return if ident.sym.as_str() == "Symbol" && ident.ctxt.has_mark(unresolved_mark) {
Some(prop)
} else {
None
};
}

if let Some(member_expr) = obj.as_member() {
// Exactly `globalThis.Symbol.something` and `globalThis` resolves
// to the global `globalThis`
if let Some(ident) = member_expr.obj.as_ident() {
if ident.sym.as_str() == "globalThis"
&& ident.ctxt.has_mark(unresolved_mark)
&& member_expr.prop.is_ident_with("Symbol")
{
return Some(prop);
}
}
}

None
}
}

pub trait PatExt {
Expand Down Expand Up @@ -88,6 +128,7 @@ impl PatExt for Pat {

pub trait PropNameExit {
fn static_name(&self) -> Option<Cow<str>>;
fn static_prop(&self, unresolved_mark: Mark) -> Option<StaticProp>;
}

impl PropNameExit for PropName {
Expand All @@ -97,23 +138,54 @@ impl PropNameExit for PropName {
PropName::Str(string) => Some(Cow::Borrowed(string.value.as_str())),
PropName::Num(number) => Some(Cow::Owned(number.value.to_string())),
PropName::BigInt(big_int) => Some(Cow::Owned(big_int.value.to_string())),
PropName::Computed(computed_prop_name) => match computed_prop_name.expr.as_ref() {
Expr::Lit(lit) => match lit {
Lit::Str(string) => Some(Cow::Borrowed(string.value.as_str())),
Lit::Bool(b) => Some(Cow::Owned(b.value.to_string())),
Lit::Null(_) => Some(Cow::Borrowed("null")),
Lit::Num(number) => Some(Cow::Owned(number.value.to_string())),
Lit::BigInt(big_int) => Some(Cow::Owned(big_int.value.to_string())),
Lit::Regex(regex) => Some(Cow::Owned(regex.exp.to_string())),
Lit::JSXText(_) => None,
},
Expr::Tpl(tpl) if tpl.exprs.is_empty() => tpl
.quasis
.first()
.and_then(|e| e.cooked.as_ref())
.map(|atom| Cow::Borrowed(atom.as_str())),
_ => None,
PropName::Computed(computed_prop_name) => computed_prop_name.static_name(),
}
}

fn static_prop(&self, unresolved_mark: Mark) -> Option<StaticProp> {
match self {
PropName::Computed(c) => c.static_prop(unresolved_mark),
prop => prop.static_name().map(Into::into).map(StaticProp::Name),
}
}
}

impl PropNameExit for ComputedPropName {
fn static_name(&self) -> Option<Cow<str>> {
match self.expr.as_ref() {
Expr::Lit(lit) => match lit {
Lit::Str(string) => Some(Cow::Borrowed(string.value.as_str())),
Lit::Bool(b) => Some(Cow::Owned(b.value.to_string())),
Lit::Null(_) => Some(Cow::Borrowed("null")),
Lit::Num(number) => Some(Cow::Owned(number.value.to_string())),
Lit::BigInt(big_int) => Some(Cow::Owned(big_int.value.to_string())),
Lit::Regex(regex) => Some(Cow::Owned(regex.exp.to_string())),
Lit::JSXText(_) => None,
},
Expr::Tpl(tpl) if tpl.exprs.is_empty() => tpl
.quasis
.first()
.and_then(|e| e.cooked.as_ref())
.map(|atom| Cow::Borrowed(atom.as_str())),
_ => None,
}
}

fn static_prop(&self, unresolved_mark: Mark) -> Option<StaticProp> {
match self.expr.as_ref() {
Expr::Member(..) | Expr::OptChain(..) => self
.expr
.get_global_symbol_prop(unresolved_mark)
.and_then(|prop| match prop {
MemberProp::Ident(ident_name) => {
Some(StaticProp::Symbol(ident_name.sym.clone()))
}
MemberProp::Computed(c) => {
c.static_name().map(Into::into).map(StaticProp::Symbol)
}
MemberProp::PrivateName(..) => None,
}),
_ => self.static_name().map(Into::into).map(StaticProp::Name),
}
}
}
Expand All @@ -129,6 +201,17 @@ impl PropNameExit for Prop {
Self::Method(method_prop) => method_prop.key.static_name(),
}
}

fn static_prop(&self, unresolved_mark: Mark) -> Option<StaticProp> {
match self {
Self::Shorthand(ident_name) => Some(StaticProp::Name(ident_name.sym.clone())),
Self::KeyValue(key_value_prop) => key_value_prop.key.static_prop(unresolved_mark),
Self::Assign(..) => None,
Self::Getter(getter_prop) => getter_prop.key.static_prop(unresolved_mark),
Self::Setter(setter_prop) => setter_prop.key.static_prop(unresolved_mark),
Self::Method(method_prop) => method_prop.key.static_prop(unresolved_mark),
}
}
}

pub trait MemberPropExt {
Expand All @@ -150,3 +233,9 @@ impl MemberPropExt for MemberProp {
}
}
}

#[derive(Eq, Hash, PartialEq, Clone)]
pub(crate) enum StaticProp {
Name(Atom),
Symbol(Atom),
}
14 changes: 14 additions & 0 deletions crates/swc_typescript/tests/fixture/symbol-getter-setter.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
```==================== .D.TS ====================

// @isolatedDeclarations: true
// @emitDeclarationOnly: true
export declare const foo: {
[Symbol.toStringTag]: string;
};
export declare class Foo {
#private;
get [Symbol.toStringTag](): string;
set [Symbol.toStringTag](value: string);
}


Loading
Loading