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
13pub 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 pub fn safety_checks(&self) -> Result<()> {
62 for ctx in self.modules.values() {
64 for (item_struct, unsafe_field) in ctx.unsafe_struct_fields() {
65 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 #[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#[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 Self::new(
181 parent_path.to_owned(),
182 parent_file.to_owned(),
183 item.ident.to_string(),
184 items,
185 )
186 }
187 None => {
188 #[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 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 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}