From de77e2985849a2985a35a9e8afbc4f2303492765 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Mon, 12 Apr 2021 16:17:39 -0700 Subject: [PATCH] lang: Syntax analyzer --- examples/misc/programs/misc/src/lib.rs | 1 + lang/derive/accounts/src/lib.rs | 13 ++++++++- lang/syn/src/analyzer/accounts.rs | 39 ++++++++++++++++++++++++++ lang/syn/src/analyzer/mod.rs | 1 + lang/syn/src/codegen/accounts.rs | 23 +++++++++++++-- lang/syn/src/lib.rs | 11 ++++++++ lang/syn/src/parser/accounts.rs | 28 +++++++++++++++--- 7 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 lang/syn/src/analyzer/accounts.rs create mode 100644 lang/syn/src/analyzer/mod.rs diff --git a/examples/misc/programs/misc/src/lib.rs b/examples/misc/programs/misc/src/lib.rs index c8abd95b..06ff1710 100644 --- a/examples/misc/programs/misc/src/lib.rs +++ b/examples/misc/programs/misc/src/lib.rs @@ -68,6 +68,7 @@ pub struct Initialize<'info> { pub struct TestOwner<'info> { #[account(owner = misc)] data: AccountInfo<'info>, + #[account(address = "asdf")] misc: AccountInfo<'info>, } diff --git a/lang/derive/accounts/src/lib.rs b/lang/derive/accounts/src/lib.rs index 46f69dfd..461dabc2 100644 --- a/lang/derive/accounts/src/lib.rs +++ b/lang/derive/accounts/src/lib.rs @@ -1,5 +1,6 @@ extern crate proc_macro; +use anchor_syn::analyzer::accounts as accounts_analyzer; use anchor_syn::codegen::accounts as accounts_codegen; use anchor_syn::parser::accounts as accounts_parser; use proc_macro::TokenStream; @@ -50,11 +51,21 @@ use syn::parse_macro_input; /// | `#[account(state = )]` | On `CpiState` structs | Checks the given state is the canonical state account for the target program. | /// | `#[account(owner = )]` | On `CpiState`, `CpiAccount`, and `AccountInfo` | Checks the account owner matches the target. | /// | `#[account(associated = , with? = , payer? = , space? = "")]` | On `ProgramAccount` | Creates an associated program account at a program derived address. `associated` is the SOL address to create the account for. `with` is an optional association, for example, a `Mint` account in the SPL token program. `payer` is an optional account to pay for the account creation, defaulting to the `associated` target if none is given. `space` is an optional literal specifying how large the account is, defaulting to the account's serialized `Default::default` size (+ 8 for the account discriminator) if none is given. When creating an associated account, a `rent` `Sysvar` and `system_program` `AccountInfo` must be present in the `Accounts` struct. | +/// | `#[account(address = "")]` | On any type deriving `ToAccountInfo` | Asserts the address of account is the given Rust literal. | +/// | `#[account(unsafe)]` | On any type deriving `Accounts` | Opts out of syntax analysis checks. | // TODO: How do we make the markdown render correctly without putting everything // on absurdly long lines? #[proc_macro_derive(Accounts, attributes(account))] pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { let strct = parse_macro_input!(item as syn::ItemStruct); - let tts = accounts_codegen::generate(accounts_parser::parse(&strct)); + // Parse the syntax. + let unsafe_accounts = accounts_parser::parse(&strct); + // Perform syntax analysis. + let accounts = match accounts_analyzer::analyze(unsafe_accounts) { + Err(err) => panic!("{}", err), // TODO: use compile_error trick. + Ok(a) => a, + }; + // Generate the new token stream. + let tts = accounts_codegen::generate(accounts); proc_macro::TokenStream::from(tts) } diff --git a/lang/syn/src/analyzer/accounts.rs b/lang/syn/src/analyzer/accounts.rs new file mode 100644 index 00000000..e3da4637 --- /dev/null +++ b/lang/syn/src/analyzer/accounts.rs @@ -0,0 +1,39 @@ +//! Perform syntax analysis on an accounts struct, providing additional compile +//! time guarantees for Anchor programs. + +use crate::{AccountField, AccountsStruct}; +use thiserror::Error; + +pub fn analyze(accs: AccountsStruct) -> Result { + CpiOwner::analyze(&accs)?; + OwnerRoot::analyze(&accs)?; + Ok(accs) +} + +#[derive(Error, Debug)] +pub enum AnalyzeError { + #[error("Owner not specified on field: {0}")] + OwnerNotSpecified(String), +} + +trait SyntaxAnalyzer { + fn analyze(accs: &AccountsStruct) -> Result<(), AnalyzeError>; +} + +// Asserts all cpi accounts have an owner specified. +struct CpiOwner; +impl SyntaxAnalyzer for CpiOwner { + fn analyze(accs: &AccountsStruct) -> Result<(), AnalyzeError> { + // TODO + Ok(()) + } +} + +// Asserts all owners have explicit program ids, or are marked unsafe. +struct OwnerRoot; +impl SyntaxAnalyzer for OwnerRoot { + fn analyze(accs: &AccountsStruct) -> Result<(), AnalyzeError> { + // TODO + Ok(()) + } +} diff --git a/lang/syn/src/analyzer/mod.rs b/lang/syn/src/analyzer/mod.rs new file mode 100644 index 00000000..9bb4894f --- /dev/null +++ b/lang/syn/src/analyzer/mod.rs @@ -0,0 +1 @@ +pub mod accounts; diff --git a/lang/syn/src/codegen/accounts.rs b/lang/syn/src/codegen/accounts.rs index d581bb85..e55337d2 100644 --- a/lang/syn/src/codegen/accounts.rs +++ b/lang/syn/src/codegen/accounts.rs @@ -1,7 +1,8 @@ use crate::{ - AccountField, AccountsStruct, CompositeField, Constraint, ConstraintAssociated, - ConstraintBelongsTo, ConstraintExecutable, ConstraintLiteral, ConstraintOwner, - ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState, Field, Ty, + AccountField, AccountsStruct, CompositeField, Constraint, ConstraintAddress, + ConstraintAssociated, ConstraintBelongsTo, ConstraintExecutable, ConstraintLiteral, + ConstraintOwner, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState, + ConstraintUnsafe, Field, Ty, }; use heck::SnakeCase; use quote::quote; @@ -380,6 +381,8 @@ pub fn generate_field_constraint(f: &Field, c: &Constraint) -> proc_macro2::Toke Constraint::Executable(c) => generate_constraint_executable(f, c), Constraint::State(c) => generate_constraint_state(f, c), Constraint::Associated(c) => generate_constraint_associated(f, c), + Constraint::Address(c) => generate_constraint_address(f, c), + Constraint::Unsafe(c) => generate_constraint_unsafe(f, c), } } @@ -641,3 +644,17 @@ fn to_seeds_tts(seeds: &[syn::Ident]) -> proc_macro2::TokenStream { } tts } + +pub fn generate_constraint_address(f: &Field, c: &ConstraintAddress) -> proc_macro2::TokenStream { + let tokens = &c.tokens; + let ident = &f.ident; + quote! { + if #ident.to_account_info().key != #tokens { + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes + } + } +} + +pub fn generate_constraint_unsafe(f: &Field, _c: &ConstraintUnsafe) -> proc_macro2::TokenStream { + quote! { /* No-op. */} +} diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index b9d6a8e5..061c5a14 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -8,6 +8,7 @@ use heck::MixedCase; use quote::quote; use std::collections::HashMap; +pub mod analyzer; pub mod codegen; #[cfg(feature = "hash")] pub mod hash; @@ -294,6 +295,8 @@ pub enum Constraint { Executable(ConstraintExecutable), State(ConstraintState), Associated(ConstraintAssociated), + Address(ConstraintAddress), + Unsafe(ConstraintUnsafe), } #[derive(Debug)] @@ -338,6 +341,14 @@ pub struct ConstraintAssociated { pub associated_target: proc_macro2::Ident, } +#[derive(Debug)] +pub struct ConstraintAddress { + pub tokens: proc_macro2::TokenStream, +} + +#[derive(Debug)] +pub struct ConstraintUnsafe {} + #[derive(Debug)] pub struct Error { pub name: String, diff --git a/lang/syn/src/parser/accounts.rs b/lang/syn/src/parser/accounts.rs index 4081c20b..72103d47 100644 --- a/lang/syn/src/parser/accounts.rs +++ b/lang/syn/src/parser/accounts.rs @@ -1,8 +1,9 @@ use crate::{ - AccountField, AccountsStruct, CompositeField, Constraint, ConstraintAssociated, - ConstraintBelongsTo, ConstraintExecutable, ConstraintLiteral, ConstraintOwner, - ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState, CpiAccountTy, - CpiStateTy, Field, ProgramAccountTy, ProgramStateTy, SysvarTy, Ty, + AccountField, AccountsStruct, CompositeField, Constraint, ConstraintAddress, + ConstraintAssociated, ConstraintBelongsTo, ConstraintExecutable, ConstraintLiteral, + ConstraintOwner, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState, + ConstraintUnsafe, CpiAccountTy, CpiStateTy, Field, ProgramAccountTy, ProgramStateTy, SysvarTy, + Ty, }; pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct { @@ -224,6 +225,9 @@ fn parse_constraints( is_signer = true; constraints.push(Constraint::Signer(ConstraintSigner {})); } + "unsafe" => { + constraints.push(Constraint::Unsafe(ConstraintUnsafe {})); + } "seeds" => { match inner_tts.next().unwrap() { proc_macro2::TokenTree::Punct(punct) => { @@ -369,6 +373,22 @@ fn parse_constraints( _ => panic!("invalid space"), } } + "address" => { + match inner_tts.next().unwrap() { + proc_macro2::TokenTree::Punct(punct) => { + assert!(punct.as_char() == '='); + punct + } + _ => panic!("invalid syntax"), + }; + let tokens = match inner_tts.next().unwrap() { + proc_macro2::TokenTree::Literal(literal) => { + literal.to_string().replace("\"", "").parse().unwrap() + } + _ => panic!("invalid address syntax"), + }; + constraints.push(Constraint::Address(ConstraintAddress { tokens })); + } _ => { panic!("invalid syntax"); }