1
1
use ruff_diagnostics:: { Diagnostic , Violation } ;
2
2
use ruff_macros:: { derive_message_formats, ViolationMetadata } ;
3
3
use ruff_python_ast:: helpers:: map_callable;
4
+ use ruff_python_ast:: name:: QualifiedName ;
4
5
use ruff_python_ast:: { self as ast, Expr } ;
5
6
use ruff_python_semantic:: analyze:: { class, function_type} ;
6
7
use ruff_python_semantic:: { ScopeKind , SemanticModel } ;
@@ -9,17 +10,17 @@ use ruff_text_size::Ranged;
9
10
use crate :: checkers:: ast:: Checker ;
10
11
11
12
/// ## What it does
12
- /// Checks for uses of the `functools.lru_cache` and `functools.cache`
13
- /// decorators on methods.
13
+ /// Checks for uses of caching decorators (e.g., `functools.lru_cache`,
14
+ /// `functools.cache`) or async caching decorators (e.g., `async_lru.alru_cache`, `aiocache.cached`,
15
+ /// `aiocache.cached_stampede`, `aiocache.multi_cached`) on methods.
14
16
///
15
17
/// ## Why is this bad?
16
- /// Using the `functools.lru_cache` and `functools. cache` decorators on methods
17
- /// can lead to memory leaks, as the global cache will retain a reference to
18
- /// the instance, preventing it from being garbage collected.
18
+ /// Using cache decorators on methods can lead to memory leaks, as the global
19
+ /// cache will retain a reference to the instance, preventing it from being
20
+ /// garbage collected.
19
21
///
20
22
/// Instead, refactor the method to depend only on its arguments and not on the
21
- /// instance of the class, or use the `@lru_cache` decorator on a function
22
- /// outside of the class.
23
+ /// instance of the class, or use the decorator on a function outside of the class.
23
24
///
24
25
/// This rule ignores instance methods on enumeration classes, as enum members
25
26
/// are singletons.
@@ -61,15 +62,65 @@ use crate::checkers::ast::Checker;
61
62
/// ## References
62
63
/// - [Python documentation: `functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache)
63
64
/// - [Python documentation: `functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache)
65
+ /// - [Github: `async-lru`](https://github.com/aio-libs/async-lru)
66
+ /// - [Github: `aiocache`](https://github.com/aio-libs/aiocache)
64
67
/// - [don't lru_cache methods!](https://www.youtube.com/watch?v=sVjtp6tGo0g)
68
+ #[ derive( Copy , Clone , Debug ) ]
69
+ pub ( crate ) enum LruDecorator {
70
+ FuncToolsLruCache ,
71
+ FunctoolsCache ,
72
+ AsyncLru ,
73
+ AiocacheCached ,
74
+ AiocacheCachedStampede ,
75
+ AiocacheMultiCached
76
+ }
77
+
78
+ impl LruDecorator {
79
+ fn from_qualified_name ( qualified_name : & QualifiedName < ' _ > ) -> Option < Self > {
80
+ match qualified_name. segments ( ) {
81
+ [ "functools" , "lru_cache" ] => Some ( Self :: FuncToolsLruCache ) ,
82
+ [ "functools" , "cache" ] => Some ( Self :: FunctoolsCache ) ,
83
+ [ "async_lru" , "alru_cache" ] => Some ( Self :: AsyncLru ) ,
84
+ [ "aiocache" , "cached" ] => Some ( Self :: AiocacheCached ) ,
85
+ [ "aiocache" , "cached_stampede" ] => Some ( Self :: AiocacheCachedStampede ) ,
86
+ [ "aiocache" , "multi_cached" ] => Some ( Self :: AiocacheMultiCached ) ,
87
+ _ => None ,
88
+ }
89
+ }
90
+ }
91
+
92
+ impl std:: fmt:: Display for LruDecorator {
93
+ fn fmt ( & self , f : & mut std:: fmt:: Formatter ) -> std:: fmt:: Result {
94
+ match self {
95
+ Self :: FuncToolsLruCache => f. write_str ( "functools.lru_cache" ) ,
96
+ Self :: FunctoolsCache => f. write_str ( "functools.cache" ) ,
97
+ Self :: AsyncLru => f. write_str ( "async_lru.alru_cache" ) ,
98
+ Self :: AiocacheCached => f. write_str ( "aiocache.cached" ) ,
99
+ Self :: AiocacheCachedStampede => f. write_str ( "aiocache.cached_stampede" ) ,
100
+ Self :: AiocacheMultiCached => f. write_str ( "aiocache.multi_cached" ) ,
101
+ }
102
+ }
103
+ }
104
+
105
+
65
106
#[ derive( ViolationMetadata ) ]
66
- pub ( crate ) struct CachedInstanceMethod ;
107
+ pub ( crate ) struct CachedInstanceMethod {
108
+ decorator_name : LruDecorator ,
109
+ }
110
+
111
+ impl CachedInstanceMethod {
112
+ pub ( crate ) fn new ( decorator_name : LruDecorator ) -> Self {
113
+ Self { decorator_name }
114
+ }
115
+ }
67
116
68
117
impl Violation for CachedInstanceMethod {
69
118
#[ derive_message_formats]
70
119
fn message ( & self ) -> String {
71
- "Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks"
72
- . to_string ( )
120
+ format ! (
121
+ "Use of `{}` on methods can lead to memory leaks" ,
122
+ self . decorator_name
123
+ )
73
124
}
74
125
}
75
126
@@ -96,25 +147,28 @@ pub(crate) fn cached_instance_method(checker: &Checker, function_def: &ast::Stmt
96
147
}
97
148
98
149
for decorator in & function_def. decorator_list {
99
- if is_cache_func ( map_callable ( & decorator. expression ) , checker. semantic ( ) ) {
100
- // If we found a cached instance method, validate (lazily) that the class is not an enum.
150
+ if let Some ( decorator_name) =
151
+ get_cache_decorator_name ( map_callable ( & decorator. expression ) , checker. semantic ( ) )
152
+ {
153
+ // Ignore if class is an enum (enum members are singletons).
101
154
if class:: is_enumeration ( class_def, checker. semantic ( ) ) {
102
155
return ;
103
156
}
104
157
105
- checker. report_diagnostic ( Diagnostic :: new ( CachedInstanceMethod , decorator. range ( ) ) ) ;
158
+ checker. report_diagnostic ( Diagnostic :: new (
159
+ CachedInstanceMethod :: new ( decorator_name) ,
160
+ decorator. range ( ) ,
161
+ ) ) ;
106
162
}
107
163
}
108
164
}
109
165
110
- /// Returns `true` if the given expression is a call to `functools.lru_cache` or `functools.cache`.
111
- fn is_cache_func ( expr : & Expr , semantic : & SemanticModel ) -> bool {
112
- semantic
113
- . resolve_qualified_name ( expr)
114
- . is_some_and ( |qualified_name| {
115
- matches ! (
116
- qualified_name. segments( ) ,
117
- [ "functools" , "lru_cache" | "cache" ]
118
- )
119
- } )
166
+ /// Returns `Some(<decorator_name>)` if the given expression is one of the known
167
+ /// cache decorators, otherwise `None`.
168
+ fn get_cache_decorator_name ( expr : & Expr , semantic : & SemanticModel ) -> Option < LruDecorator > {
169
+ if let Some ( qualified_name) = semantic. resolve_qualified_name ( expr) {
170
+ LruDecorator :: from_qualified_name ( & qualified_name)
171
+ } else {
172
+ None
173
+ }
120
174
}
0 commit comments