Skip to main content

anchor_syn/parser/
context.rs

1use {
2    proc_macro2::Span,
3    std::{
4        collections::BTreeMap,
5        path::{Path, PathBuf},
6    },
7    syn::{
8        parse::{Error as ParseError, Result},
9        Ident, ImplItem, ImplItemConst, Type, TypePath,
10    },
11};
12
13/// Crate parse context
14///
15/// Keeps track of modules defined within a crate.
16pub struct CrateContext {
17    modules: BTreeMap<String, ParsedModule>,
18}
19
20impl CrateContext {
21    pub fn parse(root: impl AsRef<Path>) -> Result<Self> {
22        Ok(CrateContext {
23            modules: ParsedModule::parse_recursive(root.as_ref())?,
24        })
25    }
26
27    pub fn consts(&self) -> impl Iterator<Item = &syn::ItemConst> {
28        self.modules.values().flat_map(|ctx| ctx.consts())
29    }
30
31    pub fn impl_consts(&self) -> impl Iterator<Item = (&Ident, &syn::ImplItemConst)> {
32        self.modules.values().flat_map(|ctx| ctx.impl_consts())
33    }
34
35    pub fn structs(&self) -> impl Iterator<Item = &syn::ItemStruct> {
36        self.modules.values().flat_map(|ctx| ctx.structs())
37    }
38
39    pub fn enums(&self) -> impl Iterator<Item = &syn::ItemEnum> {
40        self.modules.values().flat_map(|ctx| ctx.enums())
41    }
42
43    pub fn type_aliases(&self) -> impl Iterator<Item = &syn::ItemType> {
44        self.modules.values().flat_map(|ctx| ctx.type_aliases())
45    }
46
47    pub fn modules(&self) -> impl Iterator<Item = ModuleContext<'_>> {
48        self.modules.values().map(|detail| ModuleContext { detail })
49    }
50
51    pub fn root_module(&self) -> ModuleContext<'_> {
52        #[allow(
53            clippy::unwrap_used,
54            reason = "\"crate\" module is always inserted during parsing"
55        )]
56        let detail = self.modules.get("crate").unwrap();
57        ModuleContext { detail }
58    }
59
60    // Perform Anchor safety checks on the parsed create
61    pub fn safety_checks(&self) -> Result<()> {
62        // Check all structs for unsafe field types, i.e. AccountInfo and UncheckedAccount.
63        for ctx in self.modules.values() {
64            for (item_struct, unsafe_field) in ctx.unsafe_struct_fields() {
65                // Check if unsafe field type has been documented with a /// SAFETY: doc string.
66                let is_documented = unsafe_field.attrs.iter().any(|attr| {
67                    if let syn::Meta::NameValue(syn::MetaNameValue {
68                        value:
69                            syn::Expr::Lit(syn::ExprLit {
70                                lit: syn::Lit::Str(s),
71                                ..
72                            }),
73                        ..
74                    }) = &attr.meta
75                    {
76                        s.value().contains("CHECK")
77                    } else {
78                        false
79                    }
80                });
81                if !is_documented {
82                    #[allow(
83                        clippy::unwrap_used,
84                        reason = "unsafe fields always have idents (named fields only)"
85                    )]
86                    let ident = unsafe_field.ident.as_ref().unwrap();
87                    let span = ident.span();
88                    let (line, column) = ctx
89                        .field_location(item_struct, ident)
90                        .unwrap_or_else(|| (span.start().line, span.start().column));
91                    // Error if undocumented.
92                    #[allow(
93                        clippy::unwrap_used,
94                        reason = "file paths are always valid during compilation"
95                    )]
96                    let canonical = ctx.file.canonicalize().unwrap();
97                    return Err(syn::Error::new(
98                        span,
99                        format!(
100                            r#"
101        {}:{}:{}
102        Struct field "{}" is unsafe, but is not documented.
103        Please add a `/// CHECK:` doc comment explaining why no checks through types are necessary.
104        Alternatively, for reasons like quick prototyping, you may disable the safety checks
105        by using the `skip-lint` option.
106        See https://www.anchor-lang.com/docs/references/account-types#uncheckedaccountinfo for more information.
107                    "#,
108                            canonical.display(),
109                            line,
110                            column,
111                            ident,
112                        ),
113                    ));
114                };
115            }
116        }
117        Ok(())
118    }
119}
120
121/// Module parse context
122///
123/// Keeps track of items defined within a module.
124#[derive(Copy, Clone)]
125pub struct ModuleContext<'krate> {
126    detail: &'krate ParsedModule,
127}
128
129impl ModuleContext<'_> {
130    pub fn items(&self) -> impl Iterator<Item = &syn::Item> {
131        self.detail.items.iter()
132    }
133}
134struct ParsedModule {
135    name: String,
136    file: PathBuf,
137    path: String,
138    items: Vec<syn::Item>,
139}
140
141struct UnparsedModule {
142    file: PathBuf,
143    path: String,
144    name: String,
145    item: syn::ItemMod,
146}
147
148impl ParsedModule {
149    fn parse_recursive(root: &Path) -> Result<BTreeMap<String, ParsedModule>> {
150        let mut modules = BTreeMap::new();
151
152        let root_content =
153            std::fs::read_to_string(root).map_err(|e| syn::Error::new(Span::call_site(), e))?;
154        let root_file = syn::parse_file(&root_content)?;
155        let root_mod = Self::new(
156            String::new(),
157            root.to_owned(),
158            "crate".to_owned(),
159            root_file.items,
160        );
161
162        let mut unparsed = root_mod.unparsed_submodules();
163        while let Some(to_parse) = unparsed.pop() {
164            let path = format!("{}::{}", to_parse.path, to_parse.name);
165            let module = Self::from_item_mod(&to_parse.file, &path, to_parse.item)?;
166
167            unparsed.extend(module.unparsed_submodules());
168            modules.insert(format!("{}{}", module.path, to_parse.name), module);
169        }
170
171        modules.insert(root_mod.name.clone(), root_mod);
172
173        Ok(modules)
174    }
175
176    fn from_item_mod(parent_file: &Path, parent_path: &str, item: syn::ItemMod) -> Result<Self> {
177        Ok(match item.content {
178            Some((_, items)) => {
179                // The module content is within the parent file being parsed
180                Self::new(
181                    parent_path.to_owned(),
182                    parent_file.to_owned(),
183                    item.ident.to_string(),
184                    items,
185                )
186            }
187            None => {
188                // The module is referencing some other file, so we need to load that
189                // to parse the items it has.
190                #[allow(
191                    clippy::unwrap_used,
192                    reason = "file paths always have parent directories during compilation"
193                )]
194                let parent_dir = parent_file.parent().unwrap();
195                #[allow(
196                    clippy::unwrap_used,
197                    reason = "file stems are always valid UTF-8 Rust identifiers"
198                )]
199                let parent_filename = parent_file.file_stem().unwrap().to_str().unwrap();
200                let parent_mod_dir = parent_dir.join(parent_filename);
201
202                let possible_file_paths = vec![
203                    parent_dir.join(format!("{}.rs", item.ident)),
204                    parent_dir.join(format!("{}/mod.rs", item.ident)),
205                    parent_mod_dir.join(format!("{}.rs", item.ident)),
206                    parent_mod_dir.join(format!("{}/mod.rs", item.ident)),
207                ];
208
209                let mod_file_path = possible_file_paths
210                    .into_iter()
211                    .find(|p| p.exists())
212                    .ok_or_else(|| ParseError::new_spanned(&item, "could not find file"))?;
213                let mod_file_content = std::fs::read_to_string(&mod_file_path)
214                    .map_err(|_| ParseError::new_spanned(&item, "could not read file"))?;
215                let mod_file = syn::parse_file(&mod_file_content)?;
216
217                Self::new(
218                    parent_path.to_owned(),
219                    mod_file_path,
220                    item.ident.to_string(),
221                    mod_file.items,
222                )
223            }
224        })
225    }
226
227    fn new(path: String, file: PathBuf, name: String, items: Vec<syn::Item>) -> Self {
228        Self {
229            name,
230            file,
231            path,
232            items,
233        }
234    }
235
236    fn unparsed_submodules(&self) -> Vec<UnparsedModule> {
237        self.submodules()
238            .map(|item| UnparsedModule {
239                file: self.file.clone(),
240                path: self.path.clone(),
241                name: item.ident.to_string(),
242                item: item.clone(),
243            })
244            .collect()
245    }
246
247    fn submodules(&self) -> impl Iterator<Item = &syn::ItemMod> {
248        self.items.iter().filter_map(|i| match i {
249            syn::Item::Mod(item) => Some(item),
250            _ => None,
251        })
252    }
253
254    fn structs(&self) -> impl Iterator<Item = &syn::ItemStruct> {
255        self.items.iter().filter_map(|i| match i {
256            syn::Item::Struct(item) => Some(item),
257            _ => None,
258        })
259    }
260
261    /// Returns each unsafe account field together with the `Accounts` struct that contains it.
262    fn unsafe_struct_fields(&self) -> impl Iterator<Item = (&syn::ItemStruct, &syn::Field)> {
263        let accounts_filter = |item_struct: &&syn::ItemStruct| {
264            item_struct.attrs.iter().any(|attr| {
265                attr.path().is_ident("derive")
266                    && attr
267                        .parse_args_with(
268                            syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
269                        )
270                        .ok()
271                        .is_some_and(|args| {
272                            args.iter()
273                                .any(|m| matches!(m, syn::Meta::Path(p) if p.is_ident("Accounts")))
274                        })
275            })
276        };
277
278        self.structs()
279            .filter(accounts_filter)
280            .flat_map(|item_struct| {
281                item_struct.fields.iter().filter_map(move |field| {
282                    match &field.ty {
283                        syn::Type::Path(syn::TypePath {
284                            path: syn::Path { segments, .. },
285                            ..
286                        }) => {
287                            segments.len() == 1 && segments[0].ident == "UncheckedAccount"
288                                || segments[0].ident == "AccountInfo"
289                        }
290                        _ => false,
291                    }
292                    .then_some((item_struct, field))
293                })
294            })
295    }
296
297    /// Returns the source line and column of a field within an `Accounts` struct, when found.
298    fn field_location(
299        &self,
300        item_struct: &syn::ItemStruct,
301        ident: &Ident,
302    ) -> Option<(usize, usize)> {
303        let source = std::fs::read_to_string(&self.file).ok()?;
304        let struct_start = source.find(&format!("struct {}", item_struct.ident))?;
305        let field_name = ident.to_string();
306        let field_start = source[struct_start..]
307            .match_indices(&field_name)
308            .map(|(index, _)| struct_start + index)
309            .find(|index| {
310                source[..*index]
311                    .chars()
312                    .next_back()
313                    .is_none_or(|character| character != '_' && !character.is_alphanumeric())
314                    && source[*index + field_name.len()..]
315                        .trim_start()
316                        .starts_with(':')
317            })?;
318        let line = source[..field_start]
319            .bytes()
320            .filter(|byte| *byte == b'\n')
321            .count()
322            + 1;
323        let column = source[..field_start]
324            .rsplit_once('\n')
325            .map_or(field_start, |(_, line)| line.len());
326
327        Some((line, column))
328    }
329
330    fn enums(&self) -> impl Iterator<Item = &syn::ItemEnum> {
331        self.items.iter().filter_map(|i| match i {
332            syn::Item::Enum(item) => Some(item),
333            _ => None,
334        })
335    }
336
337    fn type_aliases(&self) -> impl Iterator<Item = &syn::ItemType> {
338        self.items.iter().filter_map(|i| match i {
339            syn::Item::Type(item) => Some(item),
340            _ => None,
341        })
342    }
343
344    fn consts(&self) -> impl Iterator<Item = &syn::ItemConst> {
345        self.items.iter().filter_map(|i| match i {
346            syn::Item::Const(item) => Some(item),
347            _ => None,
348        })
349    }
350
351    fn impl_consts(&self) -> impl Iterator<Item = (&Ident, &ImplItemConst)> {
352        self.items
353            .iter()
354            .filter_map(|i| match i {
355                syn::Item::Impl(syn::ItemImpl {
356                    self_ty: ty, items, ..
357                }) => {
358                    if let Type::Path(TypePath {
359                        qself: None,
360                        path: p,
361                    }) = ty.as_ref()
362                    {
363                        if let Some(ident) = p.get_ident() {
364                            let mut to_return = Vec::new();
365                            items.iter().for_each(|item| {
366                                if let ImplItem::Const(item) = item {
367                                    to_return.push((ident, item));
368                                }
369                            });
370                            Some(to_return)
371                        } else {
372                            None
373                        }
374                    } else {
375                        None
376                    }
377                }
378                _ => None,
379            })
380            .flatten()
381    }
382}