Skip to content

Commit 7b25f6e

Browse files
SONARPY-267 Rule S1172: Unused function parameters should be removed (#1091)
* SONARPY-267 Rule S1172: Unused function parameters should be removed * Fix bug and code smell and increase coverage * Address review suggestions
1 parent 816716c commit 7b25f6e

File tree

10 files changed

+5951
-15
lines changed

10 files changed

+5951
-15
lines changed

its/ruling/src/test/resources/expected/python-S1172.json

Lines changed: 5732 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ public static Iterable<Class> getChecks() {
242242
UnreadPrivateMethodsCheck.class,
243243
UnsafeHttpMethodsCheck.class,
244244
UndefinedSymbolsCheck.class,
245+
UnusedFunctionParameterCheck.class,
245246
UnusedLocalVariableCheck.class,
246247
UnusedNestedDefinitionCheck.class,
247248
UnverifiedHostnameCheck.class,

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@
2020
package org.sonar.python.checks;
2121

2222
import java.util.List;
23+
import javax.annotation.CheckForNull;
2324
import javax.annotation.Nullable;
2425
import org.sonar.plugins.python.api.tree.ArgList;
2526
import org.sonar.plugins.python.api.tree.Argument;
27+
import org.sonar.plugins.python.api.tree.CallExpression;
2628
import org.sonar.plugins.python.api.tree.ClassDef;
29+
import org.sonar.plugins.python.api.tree.Expression;
30+
import org.sonar.plugins.python.api.tree.Name;
2731
import org.sonar.plugins.python.api.tree.Tree;
2832
import org.sonar.python.api.PythonTokenType;
33+
import org.sonar.python.tree.TreeUtils;
2934

3035
public class CheckUtils {
3136

@@ -65,6 +70,7 @@ private static boolean areLeavesEquivalent(Tree leftLeaf, Tree rightLeaf) {
6570
leftLeaf.firstToken().value().equals(rightLeaf.firstToken().value());
6671
}
6772

73+
@CheckForNull
6874
public static ClassDef getParentClassDef(Tree tree) {
6975
Tree current = tree.parent();
7076
while (current != null) {
@@ -89,4 +95,13 @@ public static boolean classHasInheritance(ClassDef classDef) {
8995
}
9096
return arguments.size() != 1 || !"object".equals(arguments.get(0).firstToken().value());
9197
}
98+
99+
public static boolean containsCallToLocalsFunction(Tree tree) {
100+
return TreeUtils.hasDescendant(tree, t -> t.is(Tree.Kind.CALL_EXPR) && calleeHasNameLocals(((CallExpression) t)));
101+
}
102+
103+
private static boolean calleeHasNameLocals(CallExpression callExpression) {
104+
Expression callee = callExpression.callee();
105+
return callee.is(Tree.Kind.NAME) && "locals".equals(((Name) callee).name());
106+
}
92107
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.checks;
21+
22+
import org.sonar.check.Rule;
23+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
24+
import org.sonar.plugins.python.api.SubscriptionContext;
25+
import org.sonar.plugins.python.api.symbols.Symbol;
26+
import org.sonar.plugins.python.api.tree.ClassDef;
27+
import org.sonar.plugins.python.api.tree.ExpressionStatement;
28+
import org.sonar.plugins.python.api.tree.FunctionDef;
29+
import org.sonar.plugins.python.api.tree.Parameter;
30+
import org.sonar.plugins.python.api.tree.Tree.Kind;
31+
import org.sonar.python.semantic.SymbolUtils;
32+
33+
@Rule(key = "S1172")
34+
public class UnusedFunctionParameterCheck extends PythonSubscriptionCheck {
35+
36+
private static final String MESSAGE = "Remove the unused function parameter \"%s\".";
37+
38+
@Override
39+
public void initialize(Context context) {
40+
context.registerSyntaxNodeConsumer(Kind.FUNCDEF, ctx -> checkFunctionParameter(ctx, ((FunctionDef) ctx.syntaxNode())));
41+
}
42+
43+
private static void checkFunctionParameter(SubscriptionContext ctx, FunctionDef functionDef) {
44+
if (CheckUtils.containsCallToLocalsFunction(functionDef) || maybeOverridingMethod(functionDef) || isInterfaceMethod(functionDef)) {
45+
return;
46+
}
47+
48+
functionDef.localVariables().stream()
49+
.filter(symbol -> !"self".equals(symbol.name()))
50+
.map(Symbol::usages)
51+
.filter(usages -> usages.size() == 1 && usages.get(0).tree().parent().is(Kind.PARAMETER))
52+
.map(usages -> (Parameter) usages.get(0).tree().parent())
53+
.forEach(param -> ctx.addIssue(param, String.format(MESSAGE, param.name().name())));
54+
}
55+
56+
private static boolean isInterfaceMethod(FunctionDef functionDef) {
57+
return functionDef.body().statements().stream()
58+
.allMatch(statement -> statement.is(Kind.PASS_STMT, Kind.RAISE_STMT)
59+
|| (statement.is(Kind.EXPRESSION_STMT) && isStringExpression((ExpressionStatement) statement)));
60+
}
61+
62+
private static boolean isStringExpression(ExpressionStatement stmt) {
63+
return stmt.expressions().stream().allMatch(expr -> expr.is(Kind.STRING_LITERAL));
64+
}
65+
66+
private static boolean maybeOverridingMethod(FunctionDef functionDef) {
67+
if (functionDef.isMethodDefinition() && !SymbolUtils.isPrivateName(functionDef.name().name())) {
68+
ClassDef classDef = CheckUtils.getParentClassDef(functionDef);
69+
return classDef != null && classDef.args() != null;
70+
}
71+
return false;
72+
}
73+
}

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

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,16 @@
2424
import org.sonar.check.Rule;
2525
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2626
import org.sonar.plugins.python.api.SubscriptionContext;
27+
import org.sonar.plugins.python.api.symbols.Symbol;
28+
import org.sonar.plugins.python.api.symbols.Usage;
2729
import org.sonar.plugins.python.api.tree.AnnotatedAssignment;
28-
import org.sonar.plugins.python.api.tree.CallExpression;
2930
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
3031
import org.sonar.plugins.python.api.tree.DictCompExpression;
31-
import org.sonar.plugins.python.api.tree.Expression;
3232
import org.sonar.plugins.python.api.tree.ExpressionList;
3333
import org.sonar.plugins.python.api.tree.ForStatement;
3434
import org.sonar.plugins.python.api.tree.FunctionDef;
35-
import org.sonar.plugins.python.api.tree.Name;
3635
import org.sonar.plugins.python.api.tree.Tree;
3736
import org.sonar.plugins.python.api.tree.Tree.Kind;
38-
import org.sonar.plugins.python.api.symbols.Symbol;
39-
import org.sonar.plugins.python.api.symbols.Usage;
4037
import org.sonar.python.tree.TreeUtils;
4138

4239
@Rule(key = "S1481")
@@ -55,7 +52,7 @@ public void initialize(Context context) {
5552

5653
private static void checkLocalVars(SubscriptionContext ctx, Tree functionTree, Set<Symbol> symbols) {
5754
// https://docs.python.org/3/library/functions.html#locals
58-
if (isCallingLocalsFunction(functionTree)) {
55+
if (CheckUtils.containsCallToLocalsFunction(functionTree)) {
5956
return;
6057
}
6158
symbols.stream()
@@ -90,13 +87,4 @@ private static boolean isTupleDeclaration(Tree tree) {
9087
|| (t.is(Kind.EXPRESSION_LIST) && ((ExpressionList) t).expressions().size() > 1)
9188
|| (t.is(Kind.FOR_STMT) && ((ForStatement) t).expressions().size() > 1 && ((ForStatement) t).expressions().contains(tree))) != null;
9289
}
93-
94-
private static boolean isCallingLocalsFunction(Tree tree) {
95-
return TreeUtils.hasDescendant(tree, t -> t.is(Kind.CALL_EXPR) && calleeHasNameLocals(((CallExpression) t)));
96-
}
97-
98-
private static boolean calleeHasNameLocals(CallExpression callExpression) {
99-
Expression callee = callExpression.callee();
100-
return callee.is(Kind.NAME) && "locals".equals(((Name) callee).name());
101-
}
10290
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<p>Unused parameters are misleading. Whatever the values passed to such parameters, the behavior will be the same.</p>
2+
<h2>Noncompliant Code Example</h2>
3+
<pre>
4+
def do_something(a, b): # `a` is unused
5+
return compute(b)
6+
</pre>
7+
<h2>Compliant Solution</h2>
8+
<pre>
9+
def do_something(b):
10+
return compute(b)
11+
</pre>
12+
<h2>Exceptions</h2>
13+
<p>Functions in classes that override a class or implement interfaces are ignored.</p>
14+
<pre>
15+
class Parent(object):
16+
def get_value(self, a, b):
17+
return compute(a + b);
18+
19+
class Child(Patent):
20+
def get_value(self, a, b):
21+
return compute(b)
22+
</pre>
23+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"title": "Unused function parameters should be removed",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"unused"
11+
],
12+
"defaultSeverity": "Major",
13+
"ruleSpecification": "RSPEC-1172",
14+
"sqKey": "S1172",
15+
"scope": "All",
16+
"quickfix": "unknown"
17+
}

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
@@ -23,6 +23,7 @@
2323
"S1135",
2424
"S1143",
2525
"S1144",
26+
"S1172",
2627
"S1186",
2728
"S1192",
2829
"S1226",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.checks;
21+
22+
import org.junit.Test;
23+
import org.sonar.python.checks.utils.PythonCheckVerifier;
24+
25+
public class UnusedFunctionParameterCheckTest {
26+
27+
@Test
28+
public void test() {
29+
PythonCheckVerifier.verify("src/test/resources/checks/unusedFunctionParameter.py", new UnusedFunctionParameterCheck());
30+
}
31+
32+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
def f(a): # Noncompliant {{Remove the unused function parameter "a".}}
2+
# ^
3+
print("foo")
4+
5+
6+
def g(a):
7+
print(a)
8+
9+
10+
def f(a):
11+
return locals()
12+
13+
14+
def g(a): # Noncompliant
15+
b = 1
16+
c = 2
17+
compute(b)
18+
19+
class MyInterface:
20+
21+
def write_alignment(self, a):
22+
"""This method should be replaced by any derived class to do something
23+
useful.
24+
"""
25+
raise NotImplementedError("This object should be subclassed")
26+
27+
class Parent:
28+
def do_something(self, a, b): # Noncompliant {{Remove the unused function parameter "a".}}
29+
# ^
30+
return compute(b)
31+
32+
def do_something_else(self, a, b):
33+
return compute(a + b)
34+
35+
36+
class Child(Parent):
37+
def do_something_else(self, a, b):
38+
return compute(a)
39+
40+
41+
class AnotherChild(UnknownParent):
42+
def _private_method(self, a, b): # Noncompliant
43+
return compute(b)
44+
45+
def is_overriding_a_parent_method(self, a, b):
46+
return compute(b)
47+
48+
class ClassWithoutArgs:
49+
def do_something(self, a, b): # Noncompliant
50+
return compute(b)
51+
52+
class ClassWithoutArgs2():
53+
def do_something(self, a, b): # Noncompliant
54+
return compute(b)

0 commit comments

Comments
 (0)