Skip to content

Commit 5995a5b

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-3118 Rule S7614: AWS Lambda Handlers must not be an async function (#409)
GitOrigin-RevId: 1054006afc412ae857f38b454bd675c0c8e6b817
1 parent 859e262 commit 5995a5b

File tree

9 files changed

+208
-46
lines changed

9 files changed

+208
-46
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.sonar.check.Rule;
20+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
21+
import org.sonar.plugins.python.api.SubscriptionContext;
22+
import org.sonar.plugins.python.api.tree.FunctionDef;
23+
import org.sonar.plugins.python.api.tree.Tree;
24+
import org.sonar.python.checks.utils.AwsLambdaChecksUtils;
25+
26+
@Rule(key = "S7614")
27+
public class AsyncAwsLambdaHandlerCheck extends PythonSubscriptionCheck {
28+
29+
private static final String MESSAGE = "Remove the `async` keyword from this AWS Lambda handler definition.";
30+
31+
@Override
32+
public void initialize(Context context) {
33+
context.registerSyntaxNodeConsumer(Tree.Kind.FUNCDEF, AsyncAwsLambdaHandlerCheck::checkAsyncLambdaHandler);
34+
}
35+
36+
private static void checkAsyncLambdaHandler(SubscriptionContext ctx) {
37+
var functionDef = (FunctionDef) ctx.syntaxNode();
38+
39+
var asyncToken = functionDef.asyncKeyword();
40+
41+
if (asyncToken != null && AwsLambdaChecksUtils.isOnlyLambdaHandler(ctx, functionDef)) {
42+
ctx.addIssue(asyncToken, MESSAGE);
43+
}
44+
}
45+
}

python-checks/src/main/java/org/sonar/python/checks/OpenSourceCheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ public Stream<Class<?>> getChecks() {
125125
AsyncFunctionNotAsyncCheck.class,
126126
AsyncFunctionWithTimeoutCheck.class,
127127
AsyncioTaskNotStoredCheck.class,
128+
AsyncAwsLambdaHandlerCheck.class,
128129
AsyncLongSleepCheck.class,
129130
AsyncWithContextManagerCheck.class,
130131
AWSLambdaReservedEnvironmentVariableCheck.class,

python-checks/src/main/java/org/sonar/python/checks/utils/AwsLambdaChecksUtils.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@ public class AwsLambdaChecksUtils {
2828

2929
private AwsLambdaChecksUtils() {
3030
}
31-
32-
public static boolean isLambdaHandler(PythonVisitorContext ctx, FunctionDef functionDef) {
33-
return isLambdaHandler(ctx.projectConfiguration(), ctx.callGraph(), functionDef);
34-
}
35-
3631
public static boolean isLambdaHandler(SubscriptionContext ctx, FunctionDef functionDef) {
3732
return isLambdaHandler(ctx.projectConfiguration(), ctx.callGraph(), functionDef);
3833
}
@@ -46,6 +41,11 @@ public static boolean isLambdaHandler(ProjectConfiguration config, CallGraph cg,
4641
return false;
4742
}
4843

44+
public static boolean isOnlyLambdaHandler(SubscriptionContext ctx, FunctionDef functionDef) {
45+
return functionDef.name().typeV2() instanceof FunctionType functionType
46+
&& isLambdaHandlerFqn(ctx.projectConfiguration(), functionType.fullyQualifiedName());
47+
}
48+
4949
private static boolean isLambdaHandlerFqn(ProjectConfiguration projectConfiguration, String fqn) {
5050
return projectConfiguration.awsProjectConfiguration()
5151
.awsLambdaHandlers()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<p>This rule raises an issue when the AWS Lambda handler function is declared with <code>async</code>.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>The standard AWS Lambda Python runtime is designed to invoke a synchronous handler function. While Python’s <code>asyncio</code> library can be
4+
used inside a synchronous handler, by calling <code>asyncio.run()</code>, the handler function itself cannot be declared with <code>async def</code>.
5+
Doing so will lead to a runtime error.</p>
6+
<h2>How to fix it</h2>
7+
<p>To fix this issue, remove the <code>async</code> keyword from the handler function definition. The asynchronous part of the code can be moved to
8+
its own <code>async</code> function.</p>
9+
<h3>Code examples</h3>
10+
<h4>Noncompliant code example</h4>
11+
<pre data-diff-id="1" data-diff-type="noncompliant">
12+
def some_logic():
13+
...
14+
15+
async def lambda_handler(event, context): # Noncompliant: the handler is defined with async
16+
result = some_logic()
17+
return {"status": result}
18+
</pre>
19+
<h4>Compliant solution</h4>
20+
<pre data-diff-id="1" data-diff-type="compliant">
21+
import asyncio
22+
23+
async def some_logic():
24+
...
25+
26+
def lambda_handler(event, context):
27+
result = asyncio.run(some_logic())
28+
return {"status": result}
29+
</pre>
30+
<h2>Resources</h2>
31+
<h3>Documentation</h3>
32+
<ul>
33+
<li> AWS Documentation - <a href="https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html#python-handler-signature">Valid handler
34+
signatures for Python handlers</a> </li>
35+
</ul>
36+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "AWS Lambda handlers must not be an async function",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Minor",
11+
"ruleSpecification": "RSPEC-7614",
12+
"sqKey": "S7614",
13+
"scope": "All",
14+
"quickfix": "unknown",
15+
"code": {
16+
"impacts": {
17+
"RELIABILITY": "LOW"
18+
},
19+
"attribute": "CONVENTIONAL"
20+
}
21+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import java.util.List;
20+
import org.junit.jupiter.api.Test;
21+
import org.sonar.python.checks.utils.PythonCheckVerifier;
22+
import org.sonar.python.project.config.ProjectConfigurationBuilder;
23+
24+
class AsyncAwsLambdaHandlerCheckTest {
25+
26+
@Test
27+
void test() {
28+
PythonCheckVerifier.verify(
29+
List.of("src/test/resources/checks/asyncAwsLambdaHandler.py"),
30+
new AsyncAwsLambdaHandlerCheck(),
31+
new ProjectConfigurationBuilder()
32+
.addAwsLambdaHandler("n/a", "asyncAwsLambdaHandler.lambda_handler")
33+
.addAwsLambdaHandler("n/a", "asyncAwsLambdaHandler.async_lambda_handler")
34+
.build()
35+
);
36+
}
37+
38+
}

python-checks/src/test/java/org/sonar/python/checks/utils/AwsLambdaChecksUtilsTest.java

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,66 +17,80 @@
1717
package org.sonar.python.checks.utils;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20-
import static org.mockito.Mockito.mock;
2120

22-
import java.io.IOException;
23-
import java.nio.file.Files;
2421
import org.junit.jupiter.api.Test;
2522
import org.mockito.Mockito;
26-
import org.sonar.plugins.python.api.PythonFile;
27-
import org.sonar.plugins.python.api.PythonVisitorContext;
23+
import org.sonar.plugins.python.api.SubscriptionContext;
2824
import org.sonar.plugins.python.api.project.configuration.AwsLambdaHandlerInfo;
2925
import org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
30-
import org.sonar.plugins.python.api.tree.FileInput;
3126
import org.sonar.plugins.python.api.tree.FunctionDef;
3227
import org.sonar.plugins.python.api.tree.Name;
3328
import org.sonar.plugins.python.api.types.v2.FunctionType;
3429
import org.sonar.plugins.python.api.types.v2.PythonType;
3530
import org.sonar.python.semantic.v2.callgraph.CallGraph;
36-
import org.sonar.python.tree.FileInputImpl;
3731

3832
class AwsLambdaChecksUtilsTest {
3933
@Test
4034
void isLambdaHandlerTest_direct() {
41-
var pythonVisitorContext = pythonVisitorContext(CallGraph.EMPTY);
35+
var subscriptionContext = subscriptionContext(CallGraph.EMPTY);
4236

4337
var functionDef = functionDef("a.b.c");
44-
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDef)).isFalse();
38+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(subscriptionContext, functionDef)).isFalse();
4539

46-
pythonVisitorContext.projectConfiguration().awsProjectConfiguration().awsLambdaHandlers().add(new AwsLambdaHandlerInfo("a.b.c"));
47-
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDef)).isTrue();
40+
subscriptionContext.projectConfiguration().awsProjectConfiguration().awsLambdaHandlers()
41+
.add(new AwsLambdaHandlerInfo("a.b.c"));
42+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(subscriptionContext, functionDef)).isTrue();
4843

4944
var functionDefWithUnknownType = functionDef(PythonType.UNKNOWN);
50-
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDefWithUnknownType)).isFalse();
45+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(subscriptionContext, functionDefWithUnknownType)).isFalse();
5146
}
5247

5348
@Test
5449
void isLambdaHandlerTest_callGraph() {
5550
var callGraph = new CallGraph.Builder()
56-
.addUsage("lambda.handler", "a.b.c")
57-
.addUsage("a.b.c", "e.f.g")
58-
.build();
51+
.addUsage("lambda.handler", "a.b.c")
52+
.addUsage("a.b.c", "e.f.g")
53+
.build();
5954

60-
var pythonVisitorContext = pythonVisitorContext(callGraph);
55+
var subscriptionContext = subscriptionContext(callGraph);
6156

6257
var functionDef = functionDef("e.f.g");
6358

64-
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDef)).isFalse();
59+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(subscriptionContext, functionDef)).isFalse();
6560

66-
pythonVisitorContext.projectConfiguration().awsProjectConfiguration().awsLambdaHandlers().add(new AwsLambdaHandlerInfo("lambda.handler"));
67-
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDef)).isTrue();
61+
subscriptionContext.projectConfiguration().awsProjectConfiguration().awsLambdaHandlers()
62+
.add(new AwsLambdaHandlerInfo("lambda.handler"));
63+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(subscriptionContext, functionDef)).isTrue();
6864
}
6965

70-
private static PythonVisitorContext pythonVisitorContext(CallGraph callGraph) {
71-
PythonFile pythonFile = pythonFile("test.py");
72-
FileInput fileInput = mock(FileInputImpl.class);
73-
74-
return new PythonVisitorContext.Builder(fileInput, pythonFile)
75-
.projectConfiguration(new ProjectConfiguration())
76-
.callGraph(callGraph)
77-
.build();
66+
@Test
67+
void isOnlyLambdaHandlerTest() {
68+
var callGraph = new CallGraph.Builder()
69+
.addUsage("lambda.handler", "a.b.c")
70+
.build();
71+
72+
var subscriptionContext = subscriptionContext(callGraph);
73+
subscriptionContext.projectConfiguration().awsProjectConfiguration().awsLambdaHandlers()
74+
.add(new AwsLambdaHandlerInfo("lambda.handler"));
75+
76+
var handlerFunction = functionDef("lambda.handler");
77+
assertThat(AwsLambdaChecksUtils.isOnlyLambdaHandler(subscriptionContext, handlerFunction)).isTrue();
78+
79+
var calledFunction = functionDef("a.b.c");
80+
assertThat(AwsLambdaChecksUtils.isOnlyLambdaHandler(subscriptionContext, calledFunction)).isFalse();
81+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(subscriptionContext, calledFunction)).isTrue();
82+
83+
var unknownTypeFunction = functionDef(PythonType.UNKNOWN);
84+
assertThat(AwsLambdaChecksUtils.isOnlyLambdaHandler(subscriptionContext, unknownTypeFunction)).isFalse();
7885
}
7986

87+
private static SubscriptionContext subscriptionContext(CallGraph callGraph) {
88+
var subscriptionContext = Mockito.mock(org.sonar.plugins.python.api.SubscriptionContext.class);
89+
Mockito.when(subscriptionContext.projectConfiguration()).thenReturn(new ProjectConfiguration());
90+
Mockito.when(subscriptionContext.callGraph()).thenReturn(callGraph);
91+
92+
return subscriptionContext;
93+
}
8094

8195
private static FunctionDef functionDef(String name) {
8296
FunctionType functionNameType = Mockito.mock(FunctionType.class);
@@ -92,16 +106,4 @@ private static FunctionDef functionDef(PythonType type) {
92106
Mockito.when(functionDef.name()).thenReturn(functionName);
93107
return functionDef;
94108
}
95-
96-
private static PythonFile pythonFile(String fileName) {
97-
PythonFile pythonFile = Mockito.mock(PythonFile.class);
98-
Mockito.when(pythonFile.fileName()).thenReturn(fileName);
99-
try {
100-
Mockito.when(pythonFile.uri()).thenReturn(Files.createTempFile(fileName, "py").toUri());
101-
} catch (IOException e) {
102-
throw new IllegalStateException("Cannot create temporary file");
103-
}
104-
return pythonFile;
105-
}
106-
107109
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
async def async_lambda_handler(event, context): # Noncompliant {{Remove the `async` keyword from this AWS Lambda handler definition.}}
2+
#^[sc=1;ec=5]
3+
result = some_logic()
4+
return {"status": result}
5+
6+
def some_logic():
7+
return "some result"
8+
9+
10+
def lambda_handler(event, context):
11+
import asyncio
12+
13+
result = asyncio.run(not_a_lambda_handler())
14+
return {"status": result}
15+
16+
async def not_a_lambda_entry_point(): # Compliant - not a lambda handler
17+
pass

python-frontend/src/main/java/org/sonar/python/project/config/ProjectConfigurationBuilder.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ public ProjectConfigurationBuilder() {
3737
awsLambdaHandlersByPackage = new ConcurrentHashMap<>();
3838
}
3939

40-
public void addAwsLambdaHandler(String packageName, String fullyQualifiedName) {
40+
public ProjectConfigurationBuilder addAwsLambdaHandler(String packageName, String fullyQualifiedName) {
4141
awsLambdaHandlersByPackage.computeIfAbsent(packageName, k -> new HashSet<>()).add(fullyQualifiedName);
42+
return this;
4243
}
4344

44-
public void removePackageAwsLambdaHandlers(String packageName) {
45+
public ProjectConfigurationBuilder removePackageAwsLambdaHandlers(String packageName) {
4546
awsLambdaHandlersByPackage.remove(packageName);
47+
return this;
4648
}
4749

4850
public ProjectConfiguration build() {

0 commit comments

Comments
 (0)