lang: Syntax analyzer

This commit is contained in:
armaniferrante 2021-04-12 16:17:39 -07:00
parent 512604b85e
commit de77e29858
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
7 changed files with 108 additions and 8 deletions

View File

@ -68,6 +68,7 @@ pub struct Initialize<'info> {
pub struct TestOwner<'info> {
#[account(owner = misc)]
data: AccountInfo<'info>,
#[account(address = "asdf")]
misc: AccountInfo<'info>,
}

View File

@ -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 = <target>)]` | On `CpiState` structs | Checks the given state is the canonical state account for the target program. |
/// | `#[account(owner = <target>)]` | On `CpiState`, `CpiAccount`, and `AccountInfo` | Checks the account owner matches the target. |
/// | `#[account(associated = <target>, with? = <target>, payer? = <target>, space? = "<literal>")]` | 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 = "<literal>")]` | 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)
}

View File

@ -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<AccountsStruct, AnalyzeError> {
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(())
}
}

View File

@ -0,0 +1 @@
pub mod accounts;

View File

@ -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. */}
}

View File

@ -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,

View File

@ -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");
}