1
+ use std:: fmt:: { Display , Formatter } ;
2
+
1
3
use anyhow:: Result ;
2
4
3
- use ast:: StringLiteralFlags ;
4
5
use ruff_diagnostics:: { AlwaysFixableViolation , Diagnostic , Fix } ;
5
6
use ruff_macros:: { derive_message_formats, violation} ;
6
- use ruff_python_ast as ast;
7
7
use ruff_python_ast:: name:: QualifiedName ;
8
- use ruff_python_ast:: Expr ;
8
+ use ruff_python_ast:: { self as ast, Expr , StringLiteralFlags } ;
9
+ use ruff_python_semantic:: SemanticModel ;
9
10
use ruff_text_size:: { Ranged , TextRange } ;
10
11
11
12
use crate :: checkers:: ast:: Checker ;
@@ -43,7 +44,7 @@ use crate::settings::types::PythonVersion;
43
44
#[ violation]
44
45
pub struct UnspecifiedEncoding {
45
46
function_name : String ,
46
- mode : Mode ,
47
+ mode : ModeArgument ,
47
48
}
48
49
49
50
impl AlwaysFixableViolation for UnspecifiedEncoding {
@@ -55,10 +56,10 @@ impl AlwaysFixableViolation for UnspecifiedEncoding {
55
56
} = self ;
56
57
57
58
match mode {
58
- Mode :: Supported => {
59
+ ModeArgument :: Supported => {
59
60
format ! ( "`{function_name}` in text mode without explicit `encoding` argument" )
60
61
}
61
- Mode :: Unsupported => {
62
+ ModeArgument :: Unsupported => {
62
63
format ! ( "`{function_name}` without explicit `encoding` argument" )
63
64
}
64
65
}
@@ -71,11 +72,9 @@ impl AlwaysFixableViolation for UnspecifiedEncoding {
71
72
72
73
/// PLW1514
73
74
pub ( crate ) fn unspecified_encoding ( checker : & mut Checker , call : & ast:: ExprCall ) {
74
- let Some ( ( function_name, mode) ) = checker
75
- . semantic ( )
76
- . resolve_qualified_name ( & call. func )
77
- . filter ( |qualified_name| is_violation ( call, qualified_name) )
78
- . map ( |qualified_name| ( qualified_name. to_string ( ) , Mode :: from ( & qualified_name) ) )
75
+ let Some ( ( function_name, mode) ) = Callee :: try_from_call_expression ( call, checker. semantic ( ) )
76
+ . filter ( |segments| is_violation ( call, segments) )
77
+ . map ( |segments| ( segments. to_string ( ) , segments. mode_argument ( ) ) )
79
78
else {
80
79
return ;
81
80
} ;
@@ -97,6 +96,68 @@ pub(crate) fn unspecified_encoding(checker: &mut Checker, call: &ast::ExprCall)
97
96
checker. diagnostics . push ( diagnostic) ;
98
97
}
99
98
99
+ /// Represents the path of the function or method being called.
100
+ enum Callee < ' a > {
101
+ /// Fully-qualified symbol name of the callee.
102
+ Qualified ( QualifiedName < ' a > ) ,
103
+ /// Attribute value for the `pathlib.Path(...)` call e.g., `open` in
104
+ /// `pathlib.Path(...).open(...)`.
105
+ Pathlib ( & ' a str ) ,
106
+ }
107
+
108
+ impl < ' a > Callee < ' a > {
109
+ fn try_from_call_expression (
110
+ call : & ' a ast:: ExprCall ,
111
+ semantic : & ' a SemanticModel ,
112
+ ) -> Option < Self > {
113
+ if let Expr :: Attribute ( ast:: ExprAttribute { attr, value, .. } ) = call. func . as_ref ( ) {
114
+ // Check for `pathlib.Path(...).open(...)` or equivalent
115
+ if let Expr :: Call ( ast:: ExprCall { func, .. } ) = value. as_ref ( ) {
116
+ if semantic
117
+ . resolve_qualified_name ( func)
118
+ . is_some_and ( |qualified_name| {
119
+ matches ! ( qualified_name. segments( ) , [ "pathlib" , "Path" ] )
120
+ } )
121
+ {
122
+ return Some ( Callee :: Pathlib ( attr) ) ;
123
+ }
124
+ }
125
+ }
126
+
127
+ if let Some ( qualified_name) = semantic. resolve_qualified_name ( & call. func ) {
128
+ return Some ( Callee :: Qualified ( qualified_name) ) ;
129
+ }
130
+
131
+ None
132
+ }
133
+
134
+ fn mode_argument ( & self ) -> ModeArgument {
135
+ match self {
136
+ Callee :: Qualified ( qualified_name) => match qualified_name. segments ( ) {
137
+ [ "" | "codecs" | "_io" , "open" ] => ModeArgument :: Supported ,
138
+ [ "tempfile" , "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile" ] => {
139
+ ModeArgument :: Supported
140
+ }
141
+ [ "io" | "_io" , "TextIOWrapper" ] => ModeArgument :: Unsupported ,
142
+ _ => ModeArgument :: Unsupported ,
143
+ } ,
144
+ Callee :: Pathlib ( attr) => match * attr {
145
+ "open" => ModeArgument :: Supported ,
146
+ _ => ModeArgument :: Unsupported ,
147
+ } ,
148
+ }
149
+ }
150
+ }
151
+
152
+ impl Display for Callee < ' _ > {
153
+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
154
+ match self {
155
+ Callee :: Qualified ( qualified_name) => f. write_str ( & qualified_name. to_string ( ) ) ,
156
+ Callee :: Pathlib ( attr) => f. write_str ( & format ! ( "pathlib.Path(...).{attr}" ) ) ,
157
+ }
158
+ }
159
+ }
160
+
100
161
/// Generate an [`Edit`] for Python 3.10 and later.
101
162
fn generate_keyword_fix ( checker : & Checker , call : & ast:: ExprCall ) -> Fix {
102
163
Fix :: unsafe_edit ( add_argument (
@@ -146,7 +207,7 @@ fn is_binary_mode(expr: &Expr) -> Option<bool> {
146
207
}
147
208
148
209
/// Returns `true` if the given call lacks an explicit `encoding`.
149
- fn is_violation ( call : & ast:: ExprCall , qualified_name : & QualifiedName ) -> bool {
210
+ fn is_violation ( call : & ast:: ExprCall , qualified_name : & Callee ) -> bool {
150
211
// If we have something like `*args`, which might contain the encoding argument, abort.
151
212
if call. arguments . args . iter ( ) . any ( Expr :: is_starred_expr) {
152
213
return false ;
@@ -160,54 +221,61 @@ fn is_violation(call: &ast::ExprCall, qualified_name: &QualifiedName) -> bool {
160
221
{
161
222
return false ;
162
223
}
163
- match qualified_name. segments ( ) {
164
- [ "" | "codecs" | "_io" , "open" ] => {
165
- if let Some ( mode_arg) = call. arguments . find_argument ( "mode" , 1 ) {
166
- if is_binary_mode ( mode_arg) . unwrap_or ( true ) {
167
- // binary mode or unknown mode is no violation
168
- return false ;
224
+ match qualified_name {
225
+ Callee :: Qualified ( qualified_name) => match qualified_name. segments ( ) {
226
+ [ "" | "codecs" | "_io" , "open" ] => {
227
+ if let Some ( mode_arg) = call. arguments . find_argument ( "mode" , 1 ) {
228
+ if is_binary_mode ( mode_arg) . unwrap_or ( true ) {
229
+ // binary mode or unknown mode is no violation
230
+ return false ;
231
+ }
169
232
}
233
+ // else mode not specified, defaults to text mode
234
+ call. arguments . find_argument ( "encoding" , 3 ) . is_none ( )
170
235
}
171
- // else mode not specified, defaults to text mode
172
- call. arguments . find_argument ( "encoding" , 3 ) . is_none ( )
173
- }
174
- [ "tempfile" , "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile" ] => {
175
- let mode_pos = usize:: from ( qualified_name. segments ( ) [ 1 ] == "SpooledTemporaryFile" ) ;
176
- if let Some ( mode_arg) = call. arguments . find_argument ( "mode" , mode_pos) {
177
- if is_binary_mode ( mode_arg) . unwrap_or ( true ) {
178
- // binary mode or unknown mode is no violation
236
+ [ "tempfile" , tempfile_class @ ( "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile" ) ] =>
237
+ {
238
+ let mode_pos = usize:: from ( * tempfile_class == "SpooledTemporaryFile" ) ;
239
+ if let Some ( mode_arg) = call. arguments . find_argument ( "mode" , mode_pos) {
240
+ if is_binary_mode ( mode_arg) . unwrap_or ( true ) {
241
+ // binary mode or unknown mode is no violation
242
+ return false ;
243
+ }
244
+ } else {
245
+ // defaults to binary mode
179
246
return false ;
180
247
}
181
- } else {
182
- // defaults to binary mode
183
- return false ;
248
+ call . arguments
249
+ . find_argument ( "encoding" , mode_pos + 2 )
250
+ . is_none ( )
184
251
}
185
- call. arguments
186
- . find_argument ( "encoding" , mode_pos + 2 )
187
- . is_none ( )
188
- }
189
- [ "io" | "_io" , "TextIOWrapper" ] => call. arguments . find_argument ( "encoding" , 1 ) . is_none ( ) ,
190
- _ => false ,
252
+ [ "io" | "_io" , "TextIOWrapper" ] => {
253
+ call. arguments . find_argument ( "encoding" , 1 ) . is_none ( )
254
+ }
255
+ _ => false ,
256
+ } ,
257
+ Callee :: Pathlib ( attr) => match * attr {
258
+ "open" => {
259
+ if let Some ( mode_arg) = call. arguments . find_argument ( "mode" , 0 ) {
260
+ if is_binary_mode ( mode_arg) . unwrap_or ( true ) {
261
+ // binary mode or unknown mode is no violation
262
+ return false ;
263
+ }
264
+ }
265
+ // else mode not specified, defaults to text mode
266
+ call. arguments . find_argument ( "encoding" , 2 ) . is_none ( )
267
+ }
268
+ "read_text" => call. arguments . find_argument ( "encoding" , 0 ) . is_none ( ) ,
269
+ "write_text" => call. arguments . find_argument ( "encoding" , 1 ) . is_none ( ) ,
270
+ _ => false ,
271
+ } ,
191
272
}
192
273
}
193
274
194
275
#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
195
- enum Mode {
276
+ enum ModeArgument {
196
277
/// The call supports a `mode` argument.
197
278
Supported ,
198
279
/// The call does not support a `mode` argument.
199
280
Unsupported ,
200
281
}
201
-
202
- impl From < & QualifiedName < ' _ > > for Mode {
203
- fn from ( value : & QualifiedName < ' _ > ) -> Self {
204
- match value. segments ( ) {
205
- [ "" | "codecs" | "_io" , "open" ] => Mode :: Supported ,
206
- [ "tempfile" , "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile" ] => {
207
- Mode :: Supported
208
- }
209
- [ "io" | "_io" , "TextIOWrapper" ] => Mode :: Unsupported ,
210
- _ => Mode :: Unsupported ,
211
- }
212
- }
213
- }
0 commit comments