1
+ use crate :: checkers:: ast:: Checker ;
1
2
use ruff_diagnostics:: { Diagnostic , Edit , Fix , FixAvailability , Violation } ;
2
3
use ruff_macros:: { derive_message_formats, ViolationMetadata } ;
4
+ use ruff_python_ast:: helpers:: map_callable;
3
5
use ruff_python_ast:: {
4
6
name:: QualifiedName , Arguments , Expr , ExprAttribute , ExprCall , ExprContext , ExprName ,
5
- ExprStringLiteral , ExprSubscript , StmtClassDef ,
7
+ ExprStringLiteral , ExprSubscript , Stmt , StmtClassDef , StmtFunctionDef ,
6
8
} ;
7
9
use ruff_python_semantic:: analyze:: typing;
8
10
use ruff_python_semantic:: Modules ;
9
11
use ruff_python_semantic:: ScopeKind ;
10
12
use ruff_text_size:: Ranged ;
11
13
use ruff_text_size:: TextRange ;
12
14
13
- use crate :: checkers:: ast:: Checker ;
14
-
15
15
/// ## What it does
16
16
/// Checks for uses of deprecated Airflow functions and values.
17
17
///
@@ -71,6 +71,21 @@ impl Violation for Airflow3Removal {
71
71
}
72
72
}
73
73
74
+ const REMOVED_CONTEXT_KEYS : [ & str ; 12 ] = [
75
+ "conf" ,
76
+ "execution_date" ,
77
+ "next_ds" ,
78
+ "next_ds_nodash" ,
79
+ "next_execution_date" ,
80
+ "prev_ds" ,
81
+ "prev_ds_nodash" ,
82
+ "prev_execution_date" ,
83
+ "prev_execution_date_success" ,
84
+ "tomorrow_ds" ,
85
+ "yesterday_ds" ,
86
+ "yesterday_ds_nodash" ,
87
+ ] ;
88
+
74
89
fn extract_name_from_slice ( slice : & Expr ) -> Option < String > {
75
90
match slice {
76
91
Expr :: StringLiteral ( ExprStringLiteral { value, .. } ) => Some ( value. to_string ( ) ) ,
@@ -79,21 +94,6 @@ fn extract_name_from_slice(slice: &Expr) -> Option<String> {
79
94
}
80
95
81
96
pub ( crate ) fn removed_context_variable ( checker : & mut Checker , expr : & Expr ) {
82
- const REMOVED_CONTEXT_KEYS : [ & str ; 12 ] = [
83
- "conf" ,
84
- "execution_date" ,
85
- "next_ds" ,
86
- "next_ds_nodash" ,
87
- "next_execution_date" ,
88
- "prev_ds" ,
89
- "prev_ds_nodash" ,
90
- "prev_execution_date" ,
91
- "prev_execution_date_success" ,
92
- "tomorrow_ds" ,
93
- "yesterday_ds" ,
94
- "yesterday_ds_nodash" ,
95
- ] ;
96
-
97
97
if let Expr :: Subscript ( ExprSubscript { value, slice, .. } ) = expr {
98
98
if let Expr :: Name ( ExprName { id, .. } ) = & * * value {
99
99
if id. as_str ( ) == "context" {
@@ -144,6 +144,7 @@ pub(crate) fn removed_in_3(checker: &mut Checker, expr: &Expr) {
144
144
check_call_arguments ( checker, & qualname, arguments) ;
145
145
} ;
146
146
check_method ( checker, call_expr) ;
147
+ check_context_get ( checker, call_expr) ;
147
148
}
148
149
Expr :: Attribute ( attribute_expr @ ExprAttribute { attr, .. } ) => {
149
150
check_name ( checker, expr, attr. range ( ) ) ;
@@ -307,6 +308,52 @@ fn check_class_attribute(checker: &mut Checker, attribute_expr: &ExprAttribute)
307
308
}
308
309
}
309
310
311
+ /// Check whether a removed context key is access through context.get("key").
312
+ ///
313
+ /// ```python
314
+ /// from airflow.decorators import task
315
+ ///
316
+ ///
317
+ /// @task
318
+ /// def access_invalid_key_task_out_of_dag(**context):
319
+ /// print("access invalid key", context.get("conf"))
320
+ /// ```
321
+ fn check_context_get ( checker : & mut Checker , call_expr : & ExprCall ) {
322
+ if is_task_context_referenced ( checker, & call_expr. func ) {
323
+ return ;
324
+ }
325
+
326
+ let Expr :: Attribute ( ExprAttribute { value, attr, .. } ) = & * call_expr. func else {
327
+ return ;
328
+ } ;
329
+
330
+ // Ensure the method called on `context`
331
+ if !value
332
+ . as_name_expr ( )
333
+ . is_some_and ( |name| matches ! ( name. id. as_str( ) , "context" ) )
334
+ {
335
+ return ;
336
+ }
337
+
338
+ // Ensure the method called on `get`
339
+ if attr. as_str ( ) != "get" {
340
+ return ;
341
+ }
342
+
343
+ for removed_key in REMOVED_CONTEXT_KEYS {
344
+ if let Some ( argument) = call_expr. arguments . find_argument_value ( removed_key, 0 ) {
345
+ checker. diagnostics . push ( Diagnostic :: new (
346
+ Airflow3Removal {
347
+ deprecated : removed_key. to_string ( ) ,
348
+ replacement : Replacement :: None ,
349
+ } ,
350
+ argument. range ( ) ,
351
+ ) ) ;
352
+ return ;
353
+ }
354
+ }
355
+ }
356
+
310
357
/// Check whether a removed Airflow class method is called.
311
358
///
312
359
/// For example:
@@ -909,3 +956,55 @@ fn is_airflow_builtin_or_provider(segments: &[&str], module: &str, symbol_suffix
909
956
_ => false ,
910
957
}
911
958
}
959
+
960
+ fn is_task_context_referenced ( checker : & mut Checker , expr : & Expr ) -> bool {
961
+ let parents: Vec < _ > = checker. semantic ( ) . current_statements ( ) . collect ( ) ;
962
+
963
+ for stmt in parents {
964
+ if let Stmt :: FunctionDef ( function_def) = stmt {
965
+ if is_task_decorated_function ( checker, function_def) {
966
+ let arguments = extract_task_function_arguments ( function_def) ;
967
+
968
+ for deprecated_arg in REMOVED_CONTEXT_KEYS {
969
+ if arguments. contains ( & deprecated_arg. to_string ( ) ) {
970
+ checker. diagnostics . push ( Diagnostic :: new (
971
+ Airflow3Removal {
972
+ deprecated : deprecated_arg. to_string ( ) ,
973
+ replacement : Replacement :: None ,
974
+ } ,
975
+ expr. range ( ) ,
976
+ ) ) ;
977
+ return true ;
978
+ }
979
+ }
980
+ }
981
+ }
982
+ }
983
+
984
+ false
985
+ }
986
+
987
+ fn extract_task_function_arguments ( stmt : & StmtFunctionDef ) -> Vec < String > {
988
+ let mut arguments = Vec :: new ( ) ;
989
+
990
+ for param in & stmt. parameters . args {
991
+ arguments. push ( param. parameter . name . to_string ( ) ) ;
992
+ }
993
+
994
+ if let Some ( vararg) = & stmt. parameters . kwarg {
995
+ arguments. push ( format ! ( "**{}" , vararg. name) ) ;
996
+ }
997
+
998
+ arguments
999
+ }
1000
+
1001
+ fn is_task_decorated_function ( checker : & mut Checker , stmt : & StmtFunctionDef ) -> bool {
1002
+ stmt. decorator_list . iter ( ) . any ( |decorator| {
1003
+ checker
1004
+ . semantic ( )
1005
+ . resolve_qualified_name ( map_callable ( & decorator. expression ) )
1006
+ . is_some_and ( |qualified_name| {
1007
+ matches ! ( qualified_name. segments( ) , [ "airflow" , "decorators" , "task" ] )
1008
+ } )
1009
+ } )
1010
+ }
0 commit comments