diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 6e1be828..f82d7e3e 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -23,6 +23,9 @@ pub struct NapiFn { pub skip_typescript: bool, pub comments: Vec, pub parent_is_generator: bool, + pub writable: bool, + pub enumerable: bool, + pub configurable: bool, } #[derive(Debug, Clone)] @@ -94,6 +97,9 @@ pub struct NapiStructField { pub ty: syn::Type, pub getter: bool, pub setter: bool, + pub writable: bool, + pub enumerable: bool, + pub configurable: bool, pub comments: Vec, pub skip_typescript: bool, pub ts_type: Option, diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index 66c7f6ba..3cbfcda9 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -7,6 +7,11 @@ mod r#enum; mod r#fn; mod r#struct; +pub const PROPERTY_ATTRIBUTE_DEFAULT: i32 = 0; +pub const PROPERTY_ATTRIBUTE_WRITABLE: i32 = 1 << 0; +pub const PROPERTY_ATTRIBUTE_ENUMERABLE: i32 = 1 << 1; +pub const PROPERTY_ATTRIBUTE_CONFIGURABLE: i32 = 1 << 2; + pub trait TryToTokens { fn try_to_tokens(&self, tokens: &mut TokenStream) -> BindgenResult<()>; diff --git a/crates/backend/src/codegen/struct.rs b/crates/backend/src/codegen/struct.rs index 9a63ca3d..38a82fe6 100644 --- a/crates/backend/src/codegen/struct.rs +++ b/crates/backend/src/codegen/struct.rs @@ -695,9 +695,21 @@ impl NapiStruct { } let js_name = &field.js_name; + let mut attribute = super::PROPERTY_ATTRIBUTE_DEFAULT; + if field.writable { + attribute |= super::PROPERTY_ATTRIBUTE_WRITABLE; + } + if field.enumerable { + attribute |= super::PROPERTY_ATTRIBUTE_ENUMERABLE; + } + if field.configurable { + attribute |= super::PROPERTY_ATTRIBUTE_CONFIGURABLE; + } + let mut prop = quote! { napi::bindgen_prelude::Property::new(#js_name) .unwrap() + .with_property_attributes(napi::bindgen_prelude::PropertyAttributes::from_bits(#attribute).unwrap()) }; if field.getter { @@ -705,7 +717,7 @@ impl NapiStruct { (quote! { .with_getter(#getter_name) }).to_tokens(&mut prop); } - if field.setter { + if field.writable && field.setter { let setter_name = Ident::new(&format!("set_{}", field_name), Span::call_site()); (quote! { .with_setter(#setter_name) }).to_tokens(&mut prop); } @@ -757,9 +769,20 @@ impl NapiImpl { let intermediate_name = get_intermediate_ident(&item_str); methods.push(item.try_to_token_stream()?); + let mut attribute = super::PROPERTY_ATTRIBUTE_DEFAULT; + if item.writable { + attribute |= super::PROPERTY_ATTRIBUTE_WRITABLE; + } + if item.enumerable { + attribute |= super::PROPERTY_ATTRIBUTE_ENUMERABLE; + } + if item.configurable { + attribute |= super::PROPERTY_ATTRIBUTE_CONFIGURABLE; + } + let prop = props.entry(&item.js_name).or_insert_with(|| { quote! { - napi::bindgen_prelude::Property::new(#js_name).unwrap() + napi::bindgen_prelude::Property::new(#js_name).unwrap().with_property_attributes(napi::bindgen_prelude::PropertyAttributes::from_bits(#attribute).unwrap()) } }); diff --git a/crates/macro/src/parser/attrs.rs b/crates/macro/src/parser/attrs.rs index 54598ba1..3b3b0418 100644 --- a/crates/macro/src/parser/attrs.rs +++ b/crates/macro/src/parser/attrs.rs @@ -48,6 +48,9 @@ macro_rules! attrgen { (getter, Getter(Span, Option)), (setter, Setter(Span, Option)), (readonly, Readonly(Span)), + (enumerable, Enumerable(Span, Option)), + (writable, Writable(Span, Option)), + (configurable, Configurable(Span, Option)), (skip, Skip(Span)), (strict, Strict(Span)), (return_if_invalid, ReturnIfInvalid(Span)), @@ -116,6 +119,22 @@ macro_rules! methods { } }; + (@method $name:ident, $variant:ident(Span, Option)) => { + pub fn $name(&self) -> bool { + self.attrs + .iter() + .filter_map(|a| match &a.1 { + BindgenAttr::$variant(_, s) => { + a.0.set(true); + *s + } + _ => None, + }) + .next() + .unwrap_or(true) + } + }; + (@method $name:ident, $variant:ident(Span, Vec, Vec)) => { pub fn $name(&self) -> Option<(&[String], &[Span])> { self.attrs diff --git a/crates/macro/src/parser/mod.rs b/crates/macro/src/parser/mod.rs index 58f2ccd3..5450ff1b 100644 --- a/crates/macro/src/parser/mod.rs +++ b/crates/macro/src/parser/mod.rs @@ -111,6 +111,21 @@ impl Parse for BindgenAttr { return Ok(BindgenAttr::$variant(attr_span, val, span)) }); + (@parser $variant:ident(Span, Option)) => ({ + if let Ok(_) = input.parse::() { + let (val, _) = match input.parse::() { + Ok(str) => (str.value(), str.span()), + Err(_) => { + let ident = input.parse::()?.0; + (true, ident.span()) + } + }; + return Ok::(BindgenAttr::$variant(attr_span, Some(val))) + } else { + return Ok(BindgenAttr::$variant(attr_span, Some(true))) + } + }); + (@parser $variant:ident(Span, Vec, Vec)) => ({ input.parse::()?; let (vals, spans) = match input.parse::() { @@ -677,6 +692,9 @@ fn napi_fn_from_decl( ts_return_type: opts.ts_return_type().map(|(m, _)| m.to_owned()), skip_typescript: opts.skip_typescript().is_some(), parent_is_generator, + writable: opts.writable(), + enumerable: opts.enumerable(), + configurable: opts.configurable(), } }) } @@ -868,6 +886,9 @@ impl ConvertToAST for syn::ItemStruct { let ignored = field_opts.skip().is_some(); let readonly = field_opts.readonly().is_some(); + let writable = field_opts.writable(); + let enumerable = field_opts.enumerable(); + let configurable = field_opts.configurable(); let skip_typescript = field_opts.skip_typescript().is_some(); let ts_type = field_opts.ts_type().map(|e| e.0.to_string()); @@ -877,6 +898,9 @@ impl ConvertToAST for syn::ItemStruct { ty: field.ty.clone(), getter: !ignored, setter: !(ignored || readonly), + writable, + enumerable, + configurable, comments: extract_doc_comments(&field.attrs), skip_typescript, ts_type, diff --git a/crates/napi/Cargo.toml b/crates/napi/Cargo.toml index 3b508696..74160f43 100644 --- a/crates/napi/Cargo.toml +++ b/crates/napi/Cargo.toml @@ -51,6 +51,7 @@ tokio_time = ["tokio/time"] ctor = "0.1" once_cell = "1" thread_local = "1" +bitflags = "1" [dependencies.napi-sys] version = "2.2.2" diff --git a/crates/napi/src/js_values/object_property.rs b/crates/napi/src/js_values/object_property.rs index 9e89d949..4a777694 100644 --- a/crates/napi/src/js_values/object_property.rs +++ b/crates/napi/src/js_values/object_property.rs @@ -2,6 +2,8 @@ use std::convert::From; use std::ffi::CString; use std::ptr; +use bitflags::bitflags; + use crate::{sys, Callback, NapiRaw, Result}; #[derive(Clone)] @@ -29,31 +31,25 @@ impl Default for Property { } } -#[repr(i32)] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum PropertyAttributes { - Default = sys::PropertyAttributes::default, - Writable = sys::PropertyAttributes::writable, - Enumerable = sys::PropertyAttributes::enumerable, - Configurable = sys::PropertyAttributes::configurable, - Static = sys::PropertyAttributes::static_, +bitflags! { + pub struct PropertyAttributes: i32 { + const Default = sys::PropertyAttributes::default; + const Writable = sys::PropertyAttributes::writable; + const Enumerable = sys::PropertyAttributes::enumerable; + const Configurable = sys::PropertyAttributes::configurable; + const Static = sys::PropertyAttributes::static_; + } } impl Default for PropertyAttributes { fn default() -> Self { - PropertyAttributes::Default + PropertyAttributes::Configurable | PropertyAttributes::Enumerable | PropertyAttributes::Writable } } impl From for sys::napi_property_attributes { fn from(value: PropertyAttributes) -> Self { - match value { - PropertyAttributes::Default => sys::PropertyAttributes::default, - PropertyAttributes::Writable => sys::PropertyAttributes::writable, - PropertyAttributes::Enumerable => sys::PropertyAttributes::enumerable, - PropertyAttributes::Configurable => sys::PropertyAttributes::configurable, - PropertyAttributes::Static => sys::PropertyAttributes::static_, - } + value.bits() } } diff --git a/crates/napi/src/lib.rs b/crates/napi/src/lib.rs index 476096fc..7892567b 100644 --- a/crates/napi/src/lib.rs +++ b/crates/napi/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::all)] #![forbid(unsafe_op_in_unsafe_fn)] +#![allow(non_upper_case_globals)] //! High level Node.js [N-API](https://nodejs.org/api/n-api.html) binding //! diff --git a/examples/napi/__test__/object-attr.spec.ts b/examples/napi/__test__/object-attr.spec.ts new file mode 100644 index 00000000..81769ffe --- /dev/null +++ b/examples/napi/__test__/object-attr.spec.ts @@ -0,0 +1,15 @@ +import test from 'ava' + +import { NotWritableClass } from '../index' + +test('Not Writable Class', (t) => { + const obj = new NotWritableClass('1') + t.throws(() => { + obj.name = '2' + }) + obj.setName('2') + t.is(obj.name, '2') + t.throws(() => { + obj.setName = () => {} + }) +}) diff --git a/examples/napi/__test__/typegen.spec.ts.md b/examples/napi/__test__/typegen.spec.ts.md index 5e3dee5f..3ac6a9f6 100644 --- a/examples/napi/__test__/typegen.spec.ts.md +++ b/examples/napi/__test__/typegen.spec.ts.md @@ -277,6 +277,11 @@ Generated by [AVA](https://avajs.dev). static optionStartEnd(optional1: string | undefined | null, required: string, optional2?: string | undefined | null): string␊ static optionOnly(optional?: string | undefined | null): string␊ }␊ + export class NotWritableClass {␊ + name: string␊ + constructor(name: string)␊ + setName(name: string): void␊ + }␊ export class ClassWithFactory {␊ name: string␊ static withName(name: string): ClassWithFactory␊ diff --git a/examples/napi/__test__/typegen.spec.ts.snap b/examples/napi/__test__/typegen.spec.ts.snap index 31385caf..197cd296 100644 Binary files a/examples/napi/__test__/typegen.spec.ts.snap and b/examples/napi/__test__/typegen.spec.ts.snap differ diff --git a/examples/napi/__test__/values.spec.ts b/examples/napi/__test__/values.spec.ts index 72dc4cd5..5465716b 100644 --- a/examples/napi/__test__/values.spec.ts +++ b/examples/napi/__test__/values.spec.ts @@ -162,6 +162,13 @@ test('class', (t) => { t.is(dog.kind, Kind.Dog) t.is(dog.whoami(), 'Dog: 旺财') + t.notThrows(() => { + const rawMethod = dog.whoami + dog.whoami = function (...args) { + return rawMethod.apply(this, args) + } + }) + dog.name = '可乐' t.is(dog.name, '可乐') t.deepEqual(dog.returnOtherClass(), new Dog('Doge')) diff --git a/examples/napi/index.d.ts b/examples/napi/index.d.ts index b2500499..4a0f1e55 100644 --- a/examples/napi/index.d.ts +++ b/examples/napi/index.d.ts @@ -267,6 +267,11 @@ export class Optional { static optionStartEnd(optional1: string | undefined | null, required: string, optional2?: string | undefined | null): string static optionOnly(optional?: string | undefined | null): string } +export class NotWritableClass { + name: string + constructor(name: string) + setName(name: string): void +} export class ClassWithFactory { name: string static withName(name: string): ClassWithFactory diff --git a/examples/napi/src/class.rs b/examples/napi/src/class.rs index e01fc00b..a1ae310c 100644 --- a/examples/napi/src/class.rs +++ b/examples/napi/src/class.rs @@ -347,3 +347,17 @@ pub fn receive_object_with_class_field( ) -> Result> { Ok(object.bird) } + +#[napi(constructor)] +pub struct NotWritableClass { + #[napi(writable = false)] + pub name: String, +} + +#[napi] +impl NotWritableClass { + #[napi(writable = false)] + pub fn set_name(&mut self, name: String) { + self.name = name; + } +}