3838import org .sonar .plugins .python .api .tree .NumericLiteral ;
3939import org .sonar .plugins .python .api .tree .RaiseStatement ;
4040import org .sonar .plugins .python .api .tree .RegularArgument ;
41- import org .sonar .plugins .python .api .tree .StatementList ;
4241import org .sonar .plugins .python .api .tree .StringLiteral ;
4342import org .sonar .plugins .python .api .tree .Tree ;
43+ import org .sonar .plugins .python .api .types .v2 .FullyQualifiedNameHelper ;
4444import org .sonar .plugins .python .api .types .v2 .matchers .TypeMatcher ;
4545import org .sonar .plugins .python .api .types .v2 .matchers .TypeMatchers ;
4646import org .sonar .python .checks .utils .Expressions ;
47+ import org .sonar .python .semantic .v2 .callgraph .CallGraph ;
4748import org .sonar .python .tree .TreeUtils ;
4849
4950@ Rule (key = "S8415" )
@@ -65,14 +66,21 @@ public class FastAPIHTTPExceptionDocumentedCheck extends PythonSubscriptionCheck
6566 TypeMatchers .isType ("fastapi.exceptions.HTTPException" ),
6667 TypeMatchers .isType ("fastapi.HTTPException" ));
6768
68- private static final int MAX_RECURSION_DEPTH = 5 ;
69+ private static final int MAX_FUNCTION_CALLS = 100 ;
70+
71+ private final Set <Expression > reportedHttpExceptionCalls = new HashSet <>();
6972
7073 @ Override
7174 public void initialize (Context context ) {
72- context .registerSyntaxNodeConsumer (Tree .Kind .FUNCDEF , FastAPIHTTPExceptionDocumentedCheck ::checkFunctionDef );
75+ context .registerSyntaxNodeConsumer (Tree .Kind .FILE_INPUT , this ::init );
76+ context .registerSyntaxNodeConsumer (Tree .Kind .FUNCDEF , this ::checkFunctionDef );
77+ }
78+
79+ private void init (SubscriptionContext ctx ) {
80+ reportedHttpExceptionCalls .clear ();
7381 }
7482
75- private static void checkFunctionDef (SubscriptionContext ctx ) {
83+ private void checkFunctionDef (SubscriptionContext ctx ) {
7684 FunctionDef functionDef = (FunctionDef ) ctx .syntaxNode ();
7785
7886 DecoratorAnalysisResult analysisResult = new DecoratorAnalysis (ctx , functionDef ).analyze ();
@@ -146,16 +154,16 @@ private static Set<Integer> extractDocumentedStatusCodes(Expression responsesExp
146154
147155 return statusCodes ;
148156 }
149-
150157 }
151158
152- private static void reportUndocumentedExceptions (
159+ private void reportUndocumentedExceptions (
153160 SubscriptionContext ctx ,
154161 List <RaiseInfo > httpExceptions ,
155162 Set <Integer > documentedStatusCodes ) {
156163 for (RaiseInfo raiseInfo : httpExceptions ) {
157- if (!documentedStatusCodes .contains (raiseInfo .statusCode )) {
164+ if (!documentedStatusCodes .contains (raiseInfo .statusCode ) && ! reportedHttpExceptionCalls . contains ( raiseInfo . httpExceptionExpression ) ) {
158165 ctx .addIssue (raiseInfo .httpExceptionExpression , String .format (MESSAGE , raiseInfo .statusCode ));
166+ reportedHttpExceptionCalls .add (raiseInfo .httpExceptionExpression );
159167 }
160168 }
161169 }
@@ -169,33 +177,27 @@ private record DecoratorAnalysisResult(
169177 private static class RaiseInfoCollector {
170178 private final SubscriptionContext ctx ;
171179 private final FunctionDef functionDef ;
172- private final Set <FunctionDef > visited = new HashSet <>();
173180
174181 RaiseInfoCollector (SubscriptionContext ctx , FunctionDef functionDef ) {
175182 this .ctx = ctx ;
176183 this .functionDef = functionDef ;
177184 }
178185
179186 public List <RaiseInfo > collect () {
180- return collect (functionDef , 0 );
181- }
182-
183- private List <RaiseInfo > collect (FunctionDef functionDef , int depth ) {
184- List <RaiseInfo > result = new ArrayList <>();
187+ List <RaiseInfo > result = new ArrayList <>(HTTPExceptionVisitor .collect (ctx , functionDef ));
185188
186- if (visited .contains (functionDef ) || depth > MAX_RECURSION_DEPTH ) {
189+ String fqn = FullyQualifiedNameHelper .getFullyQualifiedName (functionDef .name ().typeV2 ()).orElse (null );
190+ if (fqn == null ) {
187191 return result ;
188192 }
189- visited .add (functionDef );
190193
191- StatementList body = functionDef .body ();
192- if (body == null ) {
193- return result ;
194- }
194+ CallGraph callGraph = ctx .callGraph ();
195195
196- HTTPExceptionVisitor visitor = new HTTPExceptionVisitor (ctx );
197- body .accept (visitor );
198- result .addAll (visitor .httpExceptions );
196+ callGraph .forwardStream (fqn )
197+ .limit (MAX_FUNCTION_CALLS )
198+ .forEach (node -> node .tree ()
199+ .flatMap (TreeUtils .toOptionalInstanceOfMapper (FunctionDef .class ))
200+ .ifPresent (calledFunction -> result .addAll (HTTPExceptionVisitor .collect (ctx , calledFunction ))));
199201
200202 return result ;
201203 }
@@ -244,6 +246,12 @@ public void visitFunctionDef(FunctionDef pyFunctionDefTree) {
244246 public void visitLambda (LambdaExpression pyLambdaExpressionTree ) {
245247 // don't decend into nested lambdas
246248 }
249+
250+ public static List <RaiseInfo > collect (SubscriptionContext ctx , FunctionDef tree ) {
251+ HTTPExceptionVisitor visitor = new HTTPExceptionVisitor (ctx );
252+ tree .body ().accept (visitor );
253+ return visitor .httpExceptions ;
254+ }
247255 }
248256
249257 private static Optional <Integer > extractStatusCode (Expression statusCodeExpr ) {
0 commit comments