1717package org .sonar .python .checks ;
1818
1919import java .util .Arrays ;
20- import java .util .HashSet ;
2120import java .util .List ;
22- import java .util .Objects ;
2321import java .util .Optional ;
24- import java .util .Set ;
2522import java .util .stream .Stream ;
2623import org .sonar .check .Rule ;
2724import org .sonar .check .RuleProperty ;
2825import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
2926import org .sonar .plugins .python .api .SubscriptionContext ;
30- import org .sonar .plugins .python .api .symbols .ClassSymbol ;
31- import org .sonar .plugins .python .api .symbols .FunctionSymbol ;
32- import org .sonar .plugins .python .api .symbols .Symbol ;
33- import org .sonar .plugins .python .api .symbols .Usage ;
27+ import org .sonar .plugins .python .api .symbols .v2 .SymbolV2 ;
28+ import org .sonar .plugins .python .api .symbols .v2 .UsageV2 ;
29+ import org .sonar .plugins .python .api .types .v2 .FunctionType ;
30+ import org .sonar .plugins .python .api .types .v2 .matchers .TypeMatcher ;
31+ import org .sonar .plugins .python .api .types .v2 .matchers .TypeMatchers ;
3432import org .sonar .plugins .python .api .tree .BinaryExpression ;
3533import org .sonar .plugins .python .api .tree .CallExpression ;
3634import org .sonar .plugins .python .api .tree .ClassDef ;
3735import org .sonar .plugins .python .api .tree .ConditionalExpression ;
36+ import org .sonar .plugins .python .api .tree .Decorator ;
3837import org .sonar .plugins .python .api .tree .Expression ;
3938import org .sonar .plugins .python .api .tree .FileInput ;
4039import org .sonar .plugins .python .api .tree .FunctionDef ;
@@ -87,10 +86,22 @@ private List<String> ignoredOperators() {
8786
8887 private static final List <Kind > unaryExpressionKinds = Arrays .asList (Kind .UNARY_PLUS , Kind .UNARY_MINUS , Kind .BITWISE_COMPLEMENT , Kind .NOT );
8988
90- private static final Set <String > ignoredContexts = new HashSet <>(List .of ("contextlib.suppress" ));
91-
9289 private static final String MESSAGE = "Remove or refactor this statement; it has no side effects." ;
9390
91+ private static final TypeMatcher EXCEPTION_CLASS_TYPE_MATCHER = TypeMatchers .isOrExtendsType ("builtins.BaseException" );
92+
93+ private static final TypeMatcher CONTEXTLIB_SUPPRESS_TYPE_MATCHER = TypeMatchers .isType ("contextlib.suppress" );
94+
95+ private static final TypeMatcher AIRFLOW_TYPE_MATCHER = TypeMatchers .any (
96+ TypeMatchers .isObjectInstanceOf ("airflow.models.baseoperator.BaseOperator" ),
97+ TypeMatchers .isObjectInstanceOf ("airflow.models.dag.DAG" ),
98+ TypeMatchers .isObjectInstanceOf ("airflow.models.taskmixin.DependencyMixin" )
99+ );
100+
101+ private static final TypeMatcher DAG_CLASS_TYPE_MATCHER = TypeMatchers .isType ("airflow.models.dag.DAG" );
102+
103+ private static final TypeMatcher DAG_DECORATOR_TYPE_MATCHER = TypeMatchers .isType ("airflow.decorators.dag" );
104+
94105 @ Override
95106 public void initialize (Context context ) {
96107 context .registerSyntaxNodeConsumer (Kind .STRING_LITERAL , this ::checkStringLiteral );
@@ -118,56 +129,55 @@ private static void checkNode(SubscriptionContext ctx) {
118129 if (parent == null || !parent .is (Kind .EXPRESSION_STMT )) {
119130 return ;
120131 }
121- if (isWithinIgnoredContext (tree )) {
132+ if (isWithinIgnoredContext (tree , ctx )) {
122133 return ;
123134 }
124135 // Safe cast because the rule only subscribes to expressions
125- if (isAnAirflowException ((Expression ) tree )) {
136+ if (isAnAirflowException ((Expression ) tree , ctx )) {
126137 return ;
127138 }
128139 ctx .addIssue (tree , MESSAGE );
129140 }
130141
131- private static boolean isAnAirflowException (Expression expression ) {
132- if (isWithinAirflowContext (expression )) {
142+ private static boolean isAnAirflowException (Expression expression , SubscriptionContext ctx ) {
143+ if (isWithinAirflowContext (expression , ctx )) {
133144 StatementList statementList = (StatementList ) TreeUtils .firstAncestorOfKind (expression , Kind .STATEMENT_LIST );
134145 return Optional .ofNullable (statementList ).map (StatementList ::statements ).map (statements -> statements .get (statements .size () - 1 ))
135146 .filter (lastStatement -> lastStatement .equals (TreeUtils .firstAncestorOfKind (expression , Kind .EXPRESSION_STMT ))).isPresent ();
136147 }
137148 return false ;
138149 }
139150
140- private static boolean isWithinIgnoredContext (Tree tree ) {
151+ private static boolean isWithinIgnoredContext (Tree tree , SubscriptionContext ctx ) {
141152 Tree withParent = TreeUtils .firstAncestorOfKind (tree , Kind .WITH_STMT );
142153 if (withParent != null ) {
143154 WithStatement withStatement = (WithStatement ) withParent ;
144155 return withStatement .withItems ().stream ()
145156 .map (WithItem ::test )
146157 .filter (item -> item .is (Kind .CALL_EXPR ))
147- .map (item -> ((CallExpression ) item ).calleeSymbol ())
148- .filter (Objects ::nonNull )
149- .anyMatch (s -> ignoredContexts .contains (s .fullyQualifiedName ()));
158+ .map (item -> ((CallExpression ) item ).callee ())
159+ .anyMatch (callee -> CONTEXTLIB_SUPPRESS_TYPE_MATCHER .isTrueFor (callee , ctx ));
150160 }
151161 return false ;
152162 }
153163
154- private static boolean isWithinAirflowContext (Tree tree ) {
164+ private static boolean isWithinAirflowContext (Tree tree , SubscriptionContext ctx ) {
155165 Tree withParent = TreeUtils .firstAncestorOfKind (tree , Kind .WITH_STMT );
156166 while (withParent != null ) {
157167 WithStatement withStatement = (WithStatement ) withParent ;
158168 if (withStatement .withItems ().stream ()
159169 .map (WithItem ::test )
160170 .filter (item -> item .is (Kind .CALL_EXPR ))
161- .map (item -> ((CallExpression ) item ).calleeSymbol ())
162- .filter (Objects ::nonNull )
163- .anyMatch (s -> "airflow.DAG" .equals (s .fullyQualifiedName ()))) {
171+ .map (item -> ((CallExpression ) item ).callee ())
172+ .anyMatch (callee -> DAG_CLASS_TYPE_MATCHER .isTrueFor (callee , ctx ))) {
164173 return true ;
165174 }
166175 withParent = TreeUtils .firstAncestorOfKind (withParent , Kind .WITH_STMT );
167176 }
168177 FunctionDef funcParent = (FunctionDef ) TreeUtils .firstAncestorOfKind (tree , Kind .FUNCDEF );
169- return funcParent != null && funcParent .decorators ().stream ().map (deco -> TreeUtils .getSymbolFromTree (deco .expression ())).filter (Optional ::isPresent )
170- .anyMatch (symbol -> "airflow.decorators.dag" .equals (symbol .get ().fullyQualifiedName ()));
178+ return funcParent != null && funcParent .decorators ().stream ()
179+ .map (Decorator ::expression )
180+ .anyMatch (expr -> DAG_DECORATOR_TYPE_MATCHER .isTrueFor (expr , ctx ));
171181 }
172182
173183 private static boolean isBooleanExpressionWithCalls (Tree tree ) {
@@ -192,25 +202,25 @@ private void checkStringLiteral(SubscriptionContext ctx) {
192202
193203 private static void checkName (SubscriptionContext ctx ) {
194204 Name name = (Name ) ctx .syntaxNode ();
195- Symbol symbol = name .symbol ();
196- if (symbol != null && symbol .is (Symbol .Kind .CLASS )) {
197- ClassSymbol classSymbol = (ClassSymbol ) symbol ;
198- // Creating an exception without raising it is covered by S3984
199- if (classSymbol .canBeOrExtend ("BaseException" )) {
200- return ;
201- }
205+ // Creating an exception without raising it is covered by S3984
206+ if (EXCEPTION_CLASS_TYPE_MATCHER .isTrueFor (name , ctx )) {
207+ return ;
208+ }
209+ // Avoid raising on useless statements made to suppress issues due to "unused" import
210+ SymbolV2 symbolV2 = name .symbolV2 ();
211+ if (symbolV2 != null && symbolV2 .usages ().stream ().anyMatch (u -> u .kind () == UsageV2 .Kind .IMPORT ) && symbolV2 .usages ().size () == 2 ) {
212+ return ;
202213 }
203- if (symbol != null && symbol .usages ().stream ().anyMatch (u -> u .kind ().equals (Usage .Kind .IMPORT )) && symbol .usages ().size () == 2 ) {
204- // Avoid raising on useless statements made to suppress issues due to "unused" import
214+ if (AIRFLOW_TYPE_MATCHER .isTrueFor (name , ctx )) {
205215 return ;
206216 }
207217 checkNode (ctx );
208218 }
209219
210220 private static void checkQualifiedExpression (SubscriptionContext ctx ) {
211221 QualifiedExpression qualifiedExpression = (QualifiedExpression ) ctx .syntaxNode ();
212- Symbol symbol = qualifiedExpression . symbol ();
213- if (symbol != null && symbol . is ( Symbol . Kind . FUNCTION ) && (( FunctionSymbol ) symbol ). decorators (). stream (). noneMatch ( d -> d . matches ( "property" )) ) {
222+ // Only raise on functions; properties are already resolved to their return type by the type inference engine
223+ if (qualifiedExpression . typeV2 () instanceof FunctionType ) {
214224 checkNode (ctx );
215225 }
216226 }
0 commit comments