Skip to content

Commit 2f2ac1a

Browse files
Fixed generated types for getters and setters (#4202)
1 parent 76776ef commit 2f2ac1a

File tree

6 files changed

+536
-59
lines changed

6 files changed

+536
-59
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@
4545
* Fixed potential `null` error when using `JsValue::as_debug_string()`.
4646
[#4192](https://github.com/rustwasm/wasm-bindgen/pull/4192)
4747

48+
* Fixed generated types when the getter and setter of a property have different types.
49+
[#4202](https://github.com/rustwasm/wasm-bindgen/pull/4202)
50+
51+
* Fixed generated types when a static getter/setter has the same name as an instance getter/setter.
52+
[#4202](https://github.com/rustwasm/wasm-bindgen/pull/4202)
53+
4854
--------------------------------------------------------------------------------
4955

5056
## [0.2.95](https://github.com/rustwasm/wasm-bindgen/compare/0.2.94...0.2.95)

crates/cli-support/src/js/mod.rs

+185-59
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ pub struct Context<'a> {
8181
}
8282

8383
#[derive(Default)]
84-
pub struct ExportedClass {
84+
struct ExportedClass {
8585
comments: String,
8686
contents: String,
8787
/// The TypeScript for the class's methods.
@@ -95,9 +95,29 @@ pub struct ExportedClass {
9595
is_inspectable: bool,
9696
/// All readable properties of the class
9797
readable_properties: Vec<String>,
98-
/// Map from field name to type as a string, docs plus whether it has a setter,
99-
/// whether it's optional and whether it's static.
100-
typescript_fields: HashMap<String, (String, String, bool, bool, bool)>,
98+
/// Map from field to information about those fields
99+
typescript_fields: HashMap<FieldLocation, FieldInfo>,
100+
}
101+
102+
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
103+
struct FieldLocation {
104+
name: String,
105+
is_static: bool,
106+
}
107+
#[derive(Debug)]
108+
struct FieldInfo {
109+
name: String,
110+
is_static: bool,
111+
order: usize,
112+
getter: Option<FieldAccessor>,
113+
setter: Option<FieldAccessor>,
114+
}
115+
/// A getter or setter for a field.
116+
#[derive(Debug)]
117+
struct FieldAccessor {
118+
ty: String,
119+
docs: String,
120+
is_optional: bool,
101121
}
102122

103123
const INITIAL_HEAP_VALUES: &[&str] = &["undefined", "null", "true", "false"];
@@ -1130,27 +1150,8 @@ __wbg_set_wasm(wasm);"
11301150
dst.push_str(&class.contents);
11311151
ts_dst.push_str(&class.typescript);
11321152

1133-
let mut fields = class.typescript_fields.keys().collect::<Vec<_>>();
1134-
fields.sort(); // make sure we have deterministic output
1135-
for name in fields {
1136-
let (ty, docs, has_setter, is_optional, is_static) = &class.typescript_fields[name];
1137-
ts_dst.push_str(docs);
1138-
ts_dst.push_str(" ");
1139-
if *is_static {
1140-
ts_dst.push_str("static ");
1141-
}
1142-
if !has_setter {
1143-
ts_dst.push_str("readonly ");
1144-
}
1145-
ts_dst.push_str(name);
1146-
if *is_optional {
1147-
ts_dst.push_str("?: ");
1148-
} else {
1149-
ts_dst.push_str(": ");
1150-
}
1151-
ts_dst.push_str(ty);
1152-
ts_dst.push_str(";\n");
1153-
}
1153+
self.write_class_field_types(class, &mut ts_dst);
1154+
11541155
dst.push_str("}\n");
11551156
ts_dst.push_str("}\n");
11561157

@@ -1164,6 +1165,124 @@ __wbg_set_wasm(wasm);"
11641165
Ok(())
11651166
}
11661167

1168+
fn write_class_field_types(&mut self, class: &ExportedClass, ts_dst: &mut String) {
1169+
let mut fields: Vec<&FieldInfo> = class.typescript_fields.values().collect();
1170+
fields.sort_by_key(|f| f.order); // make sure we have deterministic output
1171+
1172+
for FieldInfo {
1173+
name,
1174+
is_static,
1175+
getter,
1176+
setter,
1177+
..
1178+
} in fields
1179+
{
1180+
let is_static = if *is_static { "static " } else { "" };
1181+
1182+
let write_docs = |ts_dst: &mut String, docs: &str| {
1183+
if docs.is_empty() {
1184+
return;
1185+
}
1186+
// indent by 2 spaces
1187+
for line in docs.lines() {
1188+
ts_dst.push_str(" ");
1189+
ts_dst.push_str(line);
1190+
ts_dst.push('\n');
1191+
}
1192+
};
1193+
let write_getter = |ts_dst: &mut String, getter: &FieldAccessor| {
1194+
write_docs(ts_dst, &getter.docs);
1195+
ts_dst.push_str(" ");
1196+
ts_dst.push_str(is_static);
1197+
ts_dst.push_str("get ");
1198+
ts_dst.push_str(name);
1199+
ts_dst.push_str("(): ");
1200+
ts_dst.push_str(&getter.ty);
1201+
ts_dst.push_str(";\n");
1202+
};
1203+
let write_setter = |ts_dst: &mut String, setter: &FieldAccessor| {
1204+
write_docs(ts_dst, &setter.docs);
1205+
ts_dst.push_str(" ");
1206+
ts_dst.push_str(is_static);
1207+
ts_dst.push_str("set ");
1208+
ts_dst.push_str(name);
1209+
ts_dst.push_str("(value: ");
1210+
ts_dst.push_str(&setter.ty);
1211+
if setter.is_optional {
1212+
ts_dst.push_str(" | undefined");
1213+
}
1214+
ts_dst.push_str(");\n");
1215+
};
1216+
1217+
match (getter, setter) {
1218+
(None, None) => unreachable!("field without getter or setter"),
1219+
(Some(getter), None) => {
1220+
// readonly property
1221+
write_docs(ts_dst, &getter.docs);
1222+
ts_dst.push_str(" ");
1223+
ts_dst.push_str(is_static);
1224+
ts_dst.push_str("readonly ");
1225+
ts_dst.push_str(name);
1226+
ts_dst.push_str(if getter.is_optional { "?: " } else { ": " });
1227+
ts_dst.push_str(&getter.ty);
1228+
ts_dst.push_str(";\n");
1229+
}
1230+
(None, Some(setter)) => {
1231+
// write-only property
1232+
1233+
// Note: TypeScript does not handle the types of write-only
1234+
// properties correctly and will allow reads from write-only
1235+
// properties. This isn't a wasm-bindgen issue, but a
1236+
// TypeScript issue.
1237+
write_setter(ts_dst, setter);
1238+
}
1239+
(Some(getter), Some(setter)) => {
1240+
// read-write property
1241+
1242+
// Here's the tricky part. The getter and setter might have
1243+
// different types. Obviously, we can only declare a
1244+
// property as `foo: T` if both the getter and setter have
1245+
// the same type `T`. If they don't, we have to declare the
1246+
// getter and setter separately.
1247+
1248+
// We current generate types for optional arguments and
1249+
// return values differently. This is why for the field
1250+
// `foo: Option<T>`, the setter will have type `T` with
1251+
// `is_optional` set, while the getter has type
1252+
// `T | undefined`. Because of this difference, we have to
1253+
// "normalize" the type of the setter.
1254+
let same_type = if setter.is_optional {
1255+
getter.ty == setter.ty.clone() + " | undefined"
1256+
} else {
1257+
getter.ty == setter.ty
1258+
};
1259+
1260+
if same_type {
1261+
// simple property, e.g. foo: T
1262+
1263+
// Prefer the docs of the getter over the setter's
1264+
let docs = if !getter.docs.is_empty() {
1265+
&getter.docs
1266+
} else {
1267+
&setter.docs
1268+
};
1269+
write_docs(ts_dst, docs);
1270+
ts_dst.push_str(" ");
1271+
ts_dst.push_str(is_static);
1272+
ts_dst.push_str(name);
1273+
ts_dst.push_str(if setter.is_optional { "?: " } else { ": " });
1274+
ts_dst.push_str(&setter.ty);
1275+
ts_dst.push_str(";\n");
1276+
} else {
1277+
// separate getter and setter
1278+
write_getter(ts_dst, getter);
1279+
write_setter(ts_dst, setter);
1280+
}
1281+
}
1282+
};
1283+
}
1284+
}
1285+
11671286
fn expose_drop_ref(&mut self) {
11681287
if !self.should_write_global("drop_ref") {
11691288
return;
@@ -2743,16 +2862,18 @@ __wbg_set_wasm(wasm);"
27432862
prefix += "get ";
27442863
// For getters and setters, we generate a separate TypeScript definition.
27452864
if export.generate_typescript {
2746-
exported.push_accessor_ts(
2747-
&ts_docs,
2748-
name,
2865+
let location = FieldLocation {
2866+
name: name.clone(),
2867+
is_static: receiver.is_static(),
2868+
};
2869+
let accessor = FieldAccessor {
27492870
// This is only set to `None` when generating a constructor.
2750-
ts_ret_ty
2751-
.as_deref()
2752-
.expect("missing return type for getter"),
2753-
false,
2754-
receiver.is_static(),
2755-
);
2871+
ty: ts_ret_ty.expect("missing return type for getter"),
2872+
docs: ts_docs.clone(),
2873+
is_optional: false,
2874+
};
2875+
2876+
exported.push_accessor_ts(location, accessor, false);
27562877
}
27572878
// Add the getter to the list of readable fields (used to generate `toJSON`)
27582879
exported.readable_properties.push(name.clone());
@@ -2762,15 +2883,17 @@ __wbg_set_wasm(wasm);"
27622883
AuxExportedMethodKind::Setter => {
27632884
prefix += "set ";
27642885
if export.generate_typescript {
2765-
let is_optional = exported.push_accessor_ts(
2766-
&ts_docs,
2767-
name,
2768-
&ts_arg_tys[0],
2769-
true,
2770-
receiver.is_static(),
2771-
);
2772-
// Set whether the field is optional.
2773-
*is_optional = might_be_optional_field;
2886+
let location = FieldLocation {
2887+
name: name.clone(),
2888+
is_static: receiver.is_static(),
2889+
};
2890+
let accessor = FieldAccessor {
2891+
ty: ts_arg_tys[0].clone(),
2892+
docs: ts_docs.clone(),
2893+
is_optional: might_be_optional_field,
2894+
};
2895+
2896+
exported.push_accessor_ts(location, accessor, true);
27742897
}
27752898
None
27762899
}
@@ -4427,26 +4550,29 @@ impl ExportedClass {
44274550
}
44284551
}
44294552

4430-
#[allow(clippy::assigning_clones)] // Clippy's suggested fix doesn't work at MSRV.
44314553
fn push_accessor_ts(
44324554
&mut self,
4433-
docs: &str,
4434-
field: &str,
4435-
ty: &str,
4555+
location: FieldLocation,
4556+
accessor: FieldAccessor,
44364557
is_setter: bool,
4437-
is_static: bool,
4438-
) -> &mut bool {
4439-
let (ty_dst, accessor_docs, has_setter, is_optional, is_static_dst) =
4440-
self.typescript_fields.entry(field.to_string()).or_default();
4441-
4442-
*ty_dst = ty.to_string();
4443-
// Deterministic output: always use the getter's docs if available
4444-
if !docs.is_empty() && (accessor_docs.is_empty() || !is_setter) {
4445-
*accessor_docs = docs.to_owned();
4558+
) {
4559+
let size = self.typescript_fields.len();
4560+
let field = self
4561+
.typescript_fields
4562+
.entry(location)
4563+
.or_insert_with_key(|location| FieldInfo {
4564+
name: location.name.to_string(),
4565+
is_static: location.is_static,
4566+
order: size,
4567+
getter: None,
4568+
setter: None,
4569+
});
4570+
4571+
if is_setter {
4572+
field.setter = Some(accessor);
4573+
} else {
4574+
field.getter = Some(accessor);
44464575
}
4447-
*has_setter |= is_setter;
4448-
*is_static_dst = is_static;
4449-
is_optional
44504576
}
44514577
}
44524578

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
export class Foo {
4+
free(): void;
5+
x: number;
6+
y?: number;
7+
z?: number;
8+
readonly lone_getter: number | undefined;
9+
set lone_setter(value: number | undefined);
10+
/**
11+
* You will only read numbers.
12+
*/
13+
get weird(): number;
14+
/**
15+
* But you must write strings.
16+
*
17+
* Yes, this is totally fine in JS.
18+
*/
19+
set weird(value: string | undefined);
20+
/**
21+
* There can be static getters and setters too, and they can even have the
22+
* same name as instance getters and setters.
23+
*/
24+
static x?: boolean;
25+
}

0 commit comments

Comments
 (0)