Adding compile time errors
When users write bridge modules that will not compile we want to emit compile time errors that will guide them towards the right fix.
For example, if a user wrote the following bridge module:
#![allow(unused)] fn main() { #[swift_bridge::bridge] mod ffi { extern "Rust" { #[swift_bridge(InvalidAttribute)] type SomeType; } } pub struct SomeType; }
We would want to emit a compile time error along the lines of:
error: Unrecognized attribute "InvalidAttribute".
--> tests/ui/unrecognized-opaque-type-attribute.rs:8:24
|
8 | #[swift_bridge(InvalidAttribute)]
| ^^^^^^^^^^^^^^^^
This chapter shows how to add support for compile time errors.
Implementing Support for a Compile Time Error
To support a new compile time error we first write an automated UI test for the error case.
These tests live in crates/swift-bridge-macro/tests/ui
and are powered by the trybuild crate.
After adding our UI test, we create a new ParseError
variant that can be used to describe the error.
Here are a few example parse errors:
#![allow(unused)] fn main() { // via: crates/swift-bridge-ir/src/errors/parse_error.rs pub(crate) enum ParseError { ArgsIntoArgNotFound { func: ForeignItemFn, missing_arg: Ident, }, /// `extern {}` AbiNameMissing { /// `extern {}` /// ------ extern_token: Token![extern], }, /// `extern "Foo" {}` AbiNameInvalid { /// `extern "Foo" {}` /// ----- abi_name: LitStr, }, /// `fn foo (&self)` /// ---- AmbiguousSelf { self_: Receiver }, /// fn foo (bar: &Bar); /// If Bar wasn't declared using a `type Bar` declaration. UndeclaredType { ty: Type }, // ... snip ... } }
After adding a parse error variant, we write the code to generate an error message for the new variant. Here are a few examples:
#![allow(unused)] fn main() { // via: crates/swift-bridge-ir/src/errors/parse_error.rs impl Into<syn::Error> for ParseError { fn into(self) -> Error { match self { ParseError::AbiNameMissing { extern_token: extern_ident, } => Error::new_spanned( extern_ident, format!( r#"extern modules must have their abi set to "Rust" or "Swift". ``` extern "Rust" {{ ... }} extern "Swift" {{ ... }} ``` "# ), ), ParseError::UndeclaredType { ty } => { let ty_name = ty.to_token_stream().to_string(); let ty_name = ty_name.split_whitespace().last().unwrap(); let message = format!( r#"Type must be declared with `type {}`. "#, ty_name ); Error::new_spanned(ty, message) } ParseError::DeclaredBuiltInType { ty } => { let message = format!( r#"Type {} is already supported "#, ty.to_token_stream().to_string() ); Error::new_spanned(ty, message) } // ... snip ... } } } }
After adding our ParseError
we can implement just enough code to make it pass.
This typically happens in crates/swift-bridge-ir/src/parse.rs
, or one of its descendant modules.
For example, for the given UI test:
// via: crates/swift-bridge-macro/tests/ui/invalid-module-item.rs #[swift_bridge::bridge] mod ffi { use std; fn foo() {} } fn main() {}
# via: crates/swift-bridge-macro/tests/ui/invalid-module-item.stderr
error: Only `extern` blocks, structs and enums are supported.
--> tests/ui/invalid-module-item.rs:6:5
|
6 | use std;
| ^^^^^^^^
error: Only `extern` blocks, structs and enums are supported.
--> tests/ui/invalid-module-item.rs:7:5
|
7 | fn foo() {}
| ^^^^^^^^^^^
We push the ParseError
error using:
#![allow(unused)] fn main() { // via: crates/swift-bridge-ir/src/parse.rs for outer_mod_item in item_mod.content.unwrap().1 { match outer_mod_item { Item::ForeignMod(foreign_mod) => { // ... } Item::Struct(item_struct) => { // ... } Item::Enum(item_enum) => { // ... } invalid_item => { let error = ParseError::InvalidModuleItem { item: invalid_item }; errors.push(error); } }; } }