@@ -2,6 +2,7 @@ use anyhow::{anyhow, Result};
22use std:: collections:: BTreeMap ;
33use std:: path:: { Path , PathBuf } ;
44use syn:: parse:: { Error as ParseError , Result as ParseResult } ;
5+ use syn:: spanned:: Spanned ;
56use syn:: { Ident , ImplItem , ImplItemConst , Type , TypePath } ;
67
78/// Crate parse context
@@ -82,8 +83,216 @@ impl CrateContext {
8283 } ;
8384 }
8485 }
86+
87+ // Check for missing account reloads after CPI calls
88+ for ctx in self . modules . values ( ) {
89+ self . check_missing_account_reloads_simple ( ctx) ?;
90+ }
91+
8592 Ok ( ( ) )
8693 }
94+
95+ /// looking for common patterns
96+ fn check_missing_account_reloads_simple ( & self , module : & ParsedModule ) -> Result < ( ) > {
97+ for item in & module. items {
98+ if let syn:: Item :: Mod ( mod_item) = item {
99+ if mod_item
100+ . attrs
101+ . iter ( )
102+ . any ( |attr| attr. path . is_ident ( "program" ) )
103+ {
104+ if let Some ( ( _, items) ) = & mod_item. content {
105+ for item in items {
106+ match item {
107+ syn:: Item :: Fn ( fn_item) => {
108+ // Check functions for missing account reloads
109+ self . check_function_for_missing_reloads_simple (
110+ fn_item,
111+ & module. file ,
112+ ) ?;
113+ }
114+ syn:: Item :: Impl ( impl_item) => {
115+ for impl_item in & impl_item. items {
116+ if let syn:: ImplItem :: Method ( method) = impl_item {
117+ self . check_method_for_missing_reloads_simple (
118+ method,
119+ & module. file ,
120+ ) ?;
121+ }
122+ }
123+ }
124+ _ => { } // Ignore other items
125+ }
126+ }
127+ }
128+ }
129+ }
130+ }
131+ Ok ( ( ) )
132+ }
133+
134+ /// reload after CPI calls means invoke/invoke_signed
135+ fn check_function_for_missing_reloads_simple (
136+ & self ,
137+ function : & syn:: ItemFn ,
138+ file : & Path ,
139+ ) -> Result < ( ) > {
140+ let function_str = quote:: quote! { #function } . to_string ( ) ;
141+
142+ // invoke(...) patterns
143+ if function_str. contains ( "invoke" ) {
144+ let lines: Vec < & str > = function_str. lines ( ) . collect ( ) ;
145+ let mut found_invoke = false ;
146+ let mut found_account_access_after_invoke = false ;
147+ let mut found_reload = false ;
148+ let mut account_variables = Vec :: new ( ) ;
149+
150+ for line in lines. iter ( ) {
151+ let line = line. trim ( ) ;
152+
153+ if line. contains ( "invoke" ) {
154+ found_invoke = true ;
155+ found_reload = line. contains ( ".reload()" ) ;
156+ continue ;
157+ }
158+
159+ if line. contains ( "ctx.accounts" ) && line. contains ( "let" ) && line. contains ( "=" ) {
160+ if let Some ( var_name) = self . extract_account_variable_name ( line) {
161+ account_variables. push ( var_name) ;
162+ }
163+ }
164+
165+ if found_invoke {
166+ if line. contains ( ".reload()" ) {
167+ found_reload = true ;
168+ continue ;
169+ }
170+
171+ for var_name in & account_variables {
172+ if line. contains ( & format ! ( "{}." , var_name) ) && !found_reload {
173+ // Found account field access without reload after invoke
174+ found_account_access_after_invoke = true ;
175+ break ;
176+ }
177+ }
178+
179+ // Also check for direct ctx.accounts access
180+ if line. contains ( "ctx.accounts" ) && !line. contains ( ".reload()" ) && !found_reload
181+ {
182+ found_account_access_after_invoke = true ;
183+ break ;
184+ }
185+ }
186+ }
187+
188+ // If we found account access after invoke without reload, report error
189+ if found_account_access_after_invoke {
190+ return Err ( anyhow ! (
191+ r#"
192+ {}:{}:{}
193+ Missing account reload!
194+
195+ Account data is accessed after a CPI call (invoke/invoke_signed) without being reloaded.
196+ CPI calls can modify account data, so you must call `ctx.accounts.<account_name>.reload()?`
197+ after the CPI call and before accessing the account data.
198+ See https://www.anchor-lang.com/docs/account-types#reloading-accounts for more information.
199+ "# ,
200+ file. canonicalize( ) . unwrap( ) . display( ) ,
201+ function. sig. span( ) . start( ) . line,
202+ function. sig. span( ) . start( ) . column
203+ ) ) ;
204+ }
205+ }
206+
207+ Ok ( ( ) )
208+ }
209+
210+ fn check_method_for_missing_reloads_simple (
211+ & self ,
212+ method : & syn:: ImplItemMethod ,
213+ file : & Path ,
214+ ) -> Result < ( ) > {
215+ let method_str = quote:: quote! { #method } . to_string ( ) ;
216+
217+ if method_str. contains ( "invoke" ) {
218+ let lines: Vec < & str > = method_str. lines ( ) . collect ( ) ;
219+ let mut found_invoke = false ;
220+ let mut found_account_access_after_invoke = false ;
221+ let mut found_reload = false ;
222+ let mut account_variables = Vec :: new ( ) ;
223+
224+ for line in & lines {
225+ let line = line. trim ( ) ;
226+
227+ if line. contains ( "invoke" ) {
228+ found_invoke = true ;
229+ found_reload = false ;
230+ continue ;
231+ }
232+
233+ if line. contains ( "ctx.accounts" ) && line. contains ( "let" ) && line. contains ( "=" ) {
234+ if let Some ( var_name) = self . extract_account_variable_name ( line) {
235+ account_variables. push ( var_name) ;
236+ }
237+ }
238+
239+ if found_invoke {
240+ if line. contains ( ".reload()" ) {
241+ found_reload = true ;
242+ continue ;
243+ }
244+
245+ for var_name in & account_variables {
246+ if line. contains ( & format ! ( "{}." , var_name) ) && !found_reload {
247+ found_account_access_after_invoke = true ;
248+ break ;
249+ }
250+ }
251+
252+ // Also check for direct ctx.accounts access
253+ if line. contains ( "ctx.accounts" ) && !line. contains ( ".reload()" ) && !found_reload
254+ {
255+ found_account_access_after_invoke = true ;
256+ break ;
257+ }
258+ }
259+ }
260+
261+ // If we found account access after invoke without reload, report error
262+ if found_account_access_after_invoke {
263+ return Err ( anyhow ! (
264+ r#"
265+ {}:{}:{}
266+ Missing account reload!
267+
268+ Account data is accessed after a CPI call (invoke/invoke_signed) without being reloaded.
269+ CPI calls can modify account data, so you must call `ctx.accounts.<account_name>.reload()?`
270+ after the CPI call and before accessing the account data.
271+ See https://www.anchor-lang.com/docs/account-types#reloading-accounts for more information.
272+ "# ,
273+ file. canonicalize( ) . unwrap( ) . display( ) ,
274+ method. sig. span( ) . start( ) . line,
275+ method. sig. span( ) . start( ) . column
276+ ) ) ;
277+ }
278+ }
279+
280+ Ok ( ( ) )
281+ }
282+
283+ /// Extract account variable name from assignment line
284+ /// e.g., "let user_account = &mut ctx.accounts.user_account;" -> "user_account"
285+ fn extract_account_variable_name ( & self , line : & str ) -> Option < String > {
286+ if let Some ( start) = line. find ( "let " ) {
287+ if let Some ( end) = line[ start + 4 ..] . find ( " =" ) {
288+ let var_name = line[ start + 4 ..start + 4 + end] . trim ( ) ;
289+ if !var_name. is_empty ( ) {
290+ return Some ( var_name. to_string ( ) ) ;
291+ }
292+ }
293+ }
294+ None
295+ }
87296}
88297
89298/// Module parse context
0 commit comments