Skip to content

Commit 1c81a49

Browse files
sonar-nigel[bot]Vibe BotSeppli11
authored andcommitted
SONARPY-3986 Fix S905 false positive for bare expressions as marimo cell output (#1044)
Co-authored-by: Vibe Bot <vibe-bot@sonarsource.com> Co-authored-by: Sebastian Zumbrunn <sebastian.zumbrunn@sonarsource.com> GitOrigin-RevId: 5b9a81eca800422b6ad02a846ecbe2b018851fcc
1 parent c2d1910 commit 1c81a49

4 files changed

Lines changed: 119 additions & 3 deletions

File tree

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.sonar.plugins.python.api.tree.UnaryExpression;
4848
import org.sonar.plugins.python.api.tree.WithItem;
4949
import org.sonar.plugins.python.api.tree.WithStatement;
50+
import org.sonar.python.checks.utils.MarimoUtils;
5051
import org.sonar.python.tree.TreeUtils;
5152

5253
@Rule(key = "S905")
@@ -136,18 +137,42 @@ private static void checkNode(SubscriptionContext ctx) {
136137
if (isAnAirflowException((Expression) tree, ctx)) {
137138
return;
138139
}
140+
if (isAMarimoException((Expression) tree, ctx)) {
141+
return;
142+
}
139143
ctx.addIssue(tree, MESSAGE);
140144
}
141145

142146
private static boolean isAnAirflowException(Expression expression, SubscriptionContext ctx) {
143147
if (isWithinAirflowContext(expression, ctx)) {
144148
StatementList statementList = (StatementList) TreeUtils.firstAncestorOfKind(expression, Kind.STATEMENT_LIST);
145-
return Optional.ofNullable(statementList).map(StatementList::statements).map(statements -> statements.get(statements.size() - 1))
149+
return Optional.ofNullable(statementList)
150+
.map(StatementList::statements)
151+
.map(statements -> statements.get(statements.size() - 1))
146152
.filter(lastStatement -> lastStatement.equals(TreeUtils.firstAncestorOfKind(expression, Kind.EXPRESSION_STMT))).isPresent();
147153
}
148154
return false;
149155
}
150156

157+
private static boolean isAMarimoException(Expression expression, SubscriptionContext ctx) {
158+
return MarimoUtils.isTreeInMarimoCell(expression, ctx) && isLastExpressionInFunction(expression);
159+
}
160+
161+
private static boolean isLastExpressionInFunction(Expression expr) {
162+
FunctionDef functionDef = TreeUtils.firstAncestorOfClass(expr, FunctionDef.class);
163+
if (functionDef == null) {
164+
return false;
165+
}
166+
StatementList statementList = functionDef.body();
167+
168+
Tree expressionStmt = TreeUtils.firstAncestorOfKind(expr, Kind.EXPRESSION_STMT);
169+
return statementList.statements().stream()
170+
.filter(s -> s.is(Kind.EXPRESSION_STMT))
171+
.reduce((first, second) -> second)
172+
.filter(lastExprStmt -> lastExprStmt.equals(expressionStmt))
173+
.isPresent();
174+
}
175+
151176
private static boolean isWithinIgnoredContext(Tree tree, SubscriptionContext ctx) {
152177
Tree withParent = TreeUtils.firstAncestorOfKind(tree, Kind.WITH_STMT);
153178
if (withParent != null) {

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,31 @@ public static boolean isTreeInMarimoDecoratedFunction(Tree tree, SubscriptionCon
4343
return functionDef.decorators().stream().anyMatch(decorator -> isAppCellDecorator(decorator, ctx));
4444
}
4545

46+
public static boolean isTreeInMarimoCell(Tree tree, SubscriptionContext ctx) {
47+
FunctionDef functionDef = (FunctionDef) TreeUtils.firstAncestorOfKind(tree, Kind.FUNCDEF);
48+
if (functionDef == null) {
49+
return false;
50+
}
51+
return functionDef.decorators().stream().anyMatch(d -> isAppCell(d, ctx));
52+
}
53+
54+
private static boolean isAppCell(Decorator decorator, SubscriptionContext ctx) {
55+
return APP_CELL_MATCHER.isTrueFor(resolveDecoratorExpression(decorator), ctx);
56+
}
57+
58+
private static boolean isAppFunction(Decorator decorator, SubscriptionContext ctx) {
59+
return APP_FUNCTION_MATCHER.isTrueFor(resolveDecoratorExpression(decorator), ctx);
60+
}
61+
4662
private static boolean isAppCellDecorator(Decorator decorator, SubscriptionContext ctx) {
63+
return isAppCell(decorator, ctx) || isAppFunction(decorator, ctx);
64+
}
65+
66+
private static Expression resolveDecoratorExpression(Decorator decorator) {
4767
Expression expr = decorator.expression();
4868
if (expr instanceof CallExpression callExpr) {
49-
expr = callExpr.callee();
69+
return callExpr.callee();
5070
}
51-
return APP_CELL_MATCHER.isTrueFor(expr, ctx) || APP_FUNCTION_MATCHER.isTrueFor(expr, ctx);
71+
return expr;
5272
}
5373
}

python-checks/src/test/java/org/sonar/python/checks/UselessStatementCheckTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,10 @@ void test_import() {
4747
void test_ignore_manifest() {
4848
PythonCheckVerifier.verifyNoIssue("src/test/resources/checks/__manifest__.py", new UselessStatementCheck());
4949
}
50+
51+
@Test
52+
void test_marimo() {
53+
PythonCheckVerifier.verify("src/test/resources/checks/uselessStatement/uselessStatementMarimo.py", new UselessStatementCheck());
54+
}
55+
5056
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import marimo
2+
3+
app = marimo.App()
4+
5+
6+
@app.cell
7+
def _(combined_df):
8+
combined_df
9+
return
10+
11+
12+
@app.cell
13+
def _(_fig):
14+
_fig
15+
return
16+
17+
18+
@app.cell(hide_code=True)
19+
def _(result_df):
20+
result_df
21+
return
22+
23+
24+
@app.cell
25+
def _(df):
26+
df
27+
28+
29+
@app.cell
30+
def _(x, y):
31+
x # Noncompliant
32+
y
33+
return
34+
35+
36+
@app.cell
37+
def _(df):
38+
df
39+
if True: # control flow after cell output
40+
pass
41+
42+
43+
@app.cell
44+
def _(x, y):
45+
x # Noncompliant
46+
if True: # control flow after non-last expression
47+
pass
48+
y
49+
50+
51+
@app.cell
52+
def _(x):
53+
if True:
54+
x # Noncompliant
55+
56+
57+
@app.function
58+
def helper(x):
59+
x # Noncompliant
60+
return x
61+
62+
63+
def not_marimo(x):
64+
x # Noncompliant
65+
return x

0 commit comments

Comments
 (0)