1
- use ruff_diagnostics:: { AlwaysFixableViolation , Diagnostic , Edit , Fix } ;
1
+ use ruff_diagnostics:: { Diagnostic , Edit , Fix , FixAvailability , Violation } ;
2
2
use ruff_macros:: { derive_message_formats, violation} ;
3
3
use ruff_python_ast as ast;
4
4
use ruff_python_ast:: helpers:: map_callable;
@@ -59,14 +59,16 @@ use crate::settings::types::PythonVersion;
59
59
#[ violation]
60
60
pub struct FastApiNonAnnotatedDependency ;
61
61
62
- impl AlwaysFixableViolation for FastApiNonAnnotatedDependency {
62
+ impl Violation for FastApiNonAnnotatedDependency {
63
+ const FIX_AVAILABILITY : FixAvailability = FixAvailability :: Sometimes ;
64
+
63
65
#[ derive_message_formats]
64
66
fn message ( & self ) -> String {
65
67
format ! ( "FastAPI dependency without `Annotated`" )
66
68
}
67
69
68
- fn fix_title ( & self ) -> String {
69
- "Replace with `Annotated`" . to_string ( )
70
+ fn fix_title ( & self ) -> Option < String > {
71
+ Some ( "Replace with `Annotated`" . to_string ( ) )
70
72
}
71
73
}
72
74
@@ -75,64 +77,95 @@ pub(crate) fn fastapi_non_annotated_dependency(
75
77
checker : & mut Checker ,
76
78
function_def : & ast:: StmtFunctionDef ,
77
79
) {
78
- if !checker. semantic ( ) . seen_module ( Modules :: FASTAPI ) {
79
- return ;
80
- }
81
- if !is_fastapi_route ( function_def, checker. semantic ( ) ) {
80
+ if !checker. semantic ( ) . seen_module ( Modules :: FASTAPI )
81
+ || !is_fastapi_route ( function_def, checker. semantic ( ) )
82
+ {
82
83
return ;
83
84
}
85
+
86
+ let mut updatable_count = 0 ;
87
+ let mut has_non_updatable_default = false ;
88
+ let total_params = function_def. parameters . args . len ( ) ;
89
+
84
90
for parameter in & function_def. parameters . args {
91
+ let needs_update = matches ! (
92
+ ( & parameter. parameter. annotation, & parameter. default ) ,
93
+ ( Some ( _annotation) , Some ( default ) ) if is_fastapi_dependency( checker, default )
94
+ ) ;
95
+
96
+ if needs_update {
97
+ updatable_count += 1 ;
98
+ // Determine if it's safe to update this parameter:
99
+ // - if all parameters are updatable its safe.
100
+ // - if we've encountered a non-updatable parameter with a default value, it's no longer
101
+ // safe. (https://github.com/astral-sh/ruff/issues/12982)
102
+ let safe_to_update = updatable_count == total_params || !has_non_updatable_default;
103
+ create_diagnostic ( checker, parameter, safe_to_update) ;
104
+ } else if parameter. default . is_some ( ) {
105
+ has_non_updatable_default = true ;
106
+ }
107
+ }
108
+ }
109
+
110
+ fn is_fastapi_dependency ( checker : & Checker , expr : & ast:: Expr ) -> bool {
111
+ checker
112
+ . semantic ( )
113
+ . resolve_qualified_name ( map_callable ( expr) )
114
+ . is_some_and ( |qualified_name| {
115
+ matches ! (
116
+ qualified_name. segments( ) ,
117
+ [
118
+ "fastapi" ,
119
+ "Query"
120
+ | "Path"
121
+ | "Body"
122
+ | "Cookie"
123
+ | "Header"
124
+ | "File"
125
+ | "Form"
126
+ | "Depends"
127
+ | "Security"
128
+ ]
129
+ )
130
+ } )
131
+ }
132
+
133
+ fn create_diagnostic (
134
+ checker : & mut Checker ,
135
+ parameter : & ast:: ParameterWithDefault ,
136
+ safe_to_update : bool ,
137
+ ) {
138
+ let mut diagnostic = Diagnostic :: new ( FastApiNonAnnotatedDependency , parameter. range ) ;
139
+
140
+ if safe_to_update {
85
141
if let ( Some ( annotation) , Some ( default) ) =
86
142
( & parameter. parameter . annotation , & parameter. default )
87
143
{
88
- if checker
89
- . semantic ( )
90
- . resolve_qualified_name ( map_callable ( default) )
91
- . is_some_and ( |qualified_name| {
92
- matches ! (
93
- qualified_name. segments( ) ,
94
- [
95
- "fastapi" ,
96
- "Query"
97
- | "Path"
98
- | "Body"
99
- | "Cookie"
100
- | "Header"
101
- | "File"
102
- | "Form"
103
- | "Depends"
104
- | "Security"
105
- ]
106
- )
107
- } )
108
- {
109
- let mut diagnostic =
110
- Diagnostic :: new ( FastApiNonAnnotatedDependency , parameter. range ) ;
111
-
112
- diagnostic. try_set_fix ( || {
113
- let module = if checker. settings . target_version >= PythonVersion :: Py39 {
114
- "typing"
115
- } else {
116
- "typing_extensions"
117
- } ;
118
- let ( import_edit, binding) = checker. importer ( ) . get_or_import_symbol (
119
- & ImportRequest :: import_from ( module, "Annotated" ) ,
120
- function_def. start ( ) ,
121
- checker. semantic ( ) ,
122
- ) ?;
123
- let content = format ! (
124
- "{}: {}[{}, {}]" ,
125
- parameter. parameter. name. id,
126
- binding,
127
- checker. locator( ) . slice( annotation. range( ) ) ,
128
- checker. locator( ) . slice( default . range( ) )
129
- ) ;
130
- let parameter_edit = Edit :: range_replacement ( content, parameter. range ( ) ) ;
131
- Ok ( Fix :: unsafe_edits ( import_edit, [ parameter_edit] ) )
132
- } ) ;
133
-
134
- checker. diagnostics . push ( diagnostic) ;
135
- }
144
+ diagnostic. try_set_fix ( || {
145
+ let module = if checker. settings . target_version >= PythonVersion :: Py39 {
146
+ "typing"
147
+ } else {
148
+ "typing_extensions"
149
+ } ;
150
+ let ( import_edit, binding) = checker. importer ( ) . get_or_import_symbol (
151
+ & ImportRequest :: import_from ( module, "Annotated" ) ,
152
+ parameter. range . start ( ) ,
153
+ checker. semantic ( ) ,
154
+ ) ?;
155
+ let content = format ! (
156
+ "{}: {}[{}, {}]" ,
157
+ parameter. parameter. name. id,
158
+ binding,
159
+ checker. locator( ) . slice( annotation. range( ) ) ,
160
+ checker. locator( ) . slice( default . range( ) )
161
+ ) ;
162
+ let parameter_edit = Edit :: range_replacement ( content, parameter. range ) ;
163
+ Ok ( Fix :: unsafe_edits ( import_edit, [ parameter_edit] ) )
164
+ } ) ;
136
165
}
166
+ } else {
167
+ diagnostic. fix = None ;
137
168
}
169
+
170
+ checker. diagnostics . push ( diagnostic) ;
138
171
}
0 commit comments