Skip to content

Commit b1f2a3e

Browse files
sonar-nigel[bot]Sonar Vibe Botjoke1196
authored andcommitted
SONARPY-3996 Rule S8504 Property methods should have a return statement (#1025)
Co-authored-by: Sonar Vibe Bot <vibe-bot@sonarsource.com> Co-authored-by: David Kunzmann <david.kunzmann@sonarsource.com> GitOrigin-RevId: 177d15782e00e2aeb45196ab1c23e59ac4a4cfac
1 parent b947989 commit b1f2a3e

File tree

7 files changed

+448
-0
lines changed

7 files changed

+448
-0
lines changed

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
@@ -363,6 +363,7 @@ public Stream<Class<?>> getChecks() {
363363
PrivilegePolicyCheck.class,
364364
ProcessSignallingCheck.class,
365365
PropertyAccessorParameterCountCheck.class,
366+
PropertyMethodWithoutReturnCheck.class,
366367
PytzUsageCheck.class,
367368
PseudoRandomCheck.class,
368369
PublicApiIsSecuritySensitiveCheck.class,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
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.tree.FunctionDef;
22+
import org.sonar.plugins.python.api.tree.Tree;
23+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
24+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
25+
import org.sonar.python.checks.utils.CheckUtils;
26+
27+
@Rule(key = "S8504")
28+
public class PropertyMethodWithoutReturnCheck extends PythonSubscriptionCheck {
29+
30+
private static final String MESSAGE = "Add a return statement to this property method.";
31+
private static final TypeMatcher PROPERTY_MATCHER = TypeMatchers.isType("builtins.property");
32+
private static final TypeMatcher ABSTRACT_METHOD_MATCHER = TypeMatchers.isType("abc.abstractmethod");
33+
34+
@Override
35+
public void initialize(Context context) {
36+
context.registerSyntaxNodeConsumer(Tree.Kind.FUNCDEF, ctx -> {
37+
FunctionDef functionDef = (FunctionDef) ctx.syntaxNode();
38+
39+
boolean isProperty = functionDef.decorators().stream()
40+
.anyMatch(d -> PROPERTY_MATCHER.isTrueFor(d.expression(), ctx));
41+
boolean isAbstract = functionDef.decorators().stream()
42+
.anyMatch(d -> ABSTRACT_METHOD_MATCHER.isTrueFor(d.expression(), ctx));
43+
44+
if (!isProperty || isAbstract) {
45+
return;
46+
}
47+
48+
if (functionDef.body().statements().stream().allMatch(CheckUtils::isEmptyStatement)) {
49+
return;
50+
}
51+
52+
var collector = ReturnCheckUtils.ReturnStmtCollector.collect(functionDef);
53+
54+
if (collector.getReturnStmts().isEmpty() && !collector.containsYield() && !collector.raisesExceptions()) {
55+
ctx.addIssue(functionDef.name(), MESSAGE);
56+
}
57+
});
58+
}
59+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<p>This rule raises an issue when a method decorated with <code>@property</code> does not contain any <code>return</code> statement.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>In Python, the <code>@property</code> decorator transforms a method into a getter for a computed attribute. When you access a property, you expect
4+
to receive a value, just like accessing a regular attribute.</p>
5+
<p>A property method without a <code>return</code> statement will implicitly return <code>None</code>. This makes the property useless because it
6+
always provides <code>None</code> instead of a meaningful value. This behavior is almost always unintentional and indicates a bug in the code.</p>
7+
<h3>Exceptions</h3>
8+
<p>This rule does not raise an issue when the property method raises an exception or is decorated with <code>@abstractmethod</code>, as these are
9+
intentional patterns where a return value is not expected.</p>
10+
<h3>What is the potential impact?</h3>
11+
<p>When a property method lacks a return statement, it always returns <code>None</code>. This leads to:</p>
12+
<ul>
13+
<li><strong>logic errors</strong>: code that depends on the property value will behave incorrectly, potentially causing application failures or
14+
incorrect results.</li>
15+
<li><strong>difficult debugging</strong>: the implicit <code>None</code> return is not obvious from looking at the property access, making bugs
16+
harder to trace.</li>
17+
<li><strong>API contract violation</strong>: users of the class expect the property to provide meaningful data, but receive <code>None</code>
18+
instead, breaking the expected interface.</li>
19+
</ul>
20+
<h2>How to fix it</h2>
21+
<p>Add a <code>return</code> statement to the property method that returns the intended value.</p>
22+
<h3>Code examples</h3>
23+
<h4>Noncompliant code example</h4>
24+
<pre data-diff-id="1" data-diff-type="noncompliant">
25+
@property
26+
def area(self):
27+
3.14159 * self._radius ** 2 # Noncompliant
28+
</pre>
29+
<h4>Compliant solution</h4>
30+
<pre data-diff-id="1" data-diff-type="compliant">
31+
@property
32+
def area(self):
33+
return 3.14159 * self._radius ** 2
34+
</pre>
35+
<h2>Resources</h2>
36+
<h3>Documentation</h3>
37+
<ul>
38+
<li>Python Documentation - <a href="https://docs.python.org/3/library/functions.html#property"><code>property</code></a></li>
39+
<li>Python Documentation - <a href="https://docs.python.org/3/howto/descriptor.html#properties">Properties</a></li>
40+
</ul>
41+
<h3>Related rules</h3>
42+
<ul>
43+
<li>{rule:python:S3801} - Functions should use "return" consistently</li>
44+
</ul>
45+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "Property methods should have a return statement",
3+
"type": "BUG",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"pitfall"
11+
],
12+
"defaultSeverity": "Major",
13+
"ruleSpecification": "RSPEC-8504",
14+
"sqKey": "S8504",
15+
"scope": "All",
16+
"quickfix": "targeted",
17+
"code": {
18+
"impacts": {
19+
"RELIABILITY": "HIGH"
20+
},
21+
"attribute": "LOGICAL"
22+
}
23+
}

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@
328328
"S8413",
329329
"S8414",
330330
"S8415",
331+
"S8504",
331332
"S8517"
332333
]
333334
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
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.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class PropertyMethodWithoutReturnCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/propertyMethodWithoutReturn.py", new PropertyMethodWithoutReturnCheck());
27+
}
28+
29+
}

0 commit comments

Comments
 (0)