Skip to content

Commit 22ca118

Browse files
marc-jasper-sonarsourceclaude
authored andcommitted
SONARPY-3757 Create rule S8437: Class-Based Views should override get_context_data correctly (#866)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> GitOrigin-RevId: e59e83f595293c11c9552a7ba7a602fc337f1716
1 parent c354706 commit 22ca118

17 files changed

Lines changed: 189 additions & 9 deletions

File tree

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.sonar.plugins.python.api.tree.Expression;
2525
import org.sonar.plugins.python.api.tree.FunctionDef;
2626
import org.sonar.plugins.python.api.tree.Tree;
27-
import org.sonar.plugins.python.api.tree.UnpackingExpression;
2827
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
2928
import org.sonar.python.tree.TreeUtils;
3029

@@ -74,9 +73,6 @@ private static boolean hasMethodsParameter(CallExpression callExpr) {
7473
return true;
7574
}
7675

77-
return callExpr.arguments().stream()
78-
.filter(UnpackingExpression.class::isInstance)
79-
.map(UnpackingExpression.class::cast)
80-
.anyMatch(unpacking -> "**".equals(unpacking.starToken().value()));
76+
return callExpr.arguments().stream().anyMatch(TreeUtils::isDoubleStarExpression);
8177
}
8278
}

python-commons/src/main/java/org/sonar/plugins/python/indexer/SetupPySourceRoots.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public void visitCallExpression(CallExpression callExpression) {
149149
*/
150150
private void extractFromUnpackingArguments(CallExpression callExpression) {
151151
for (Argument argument : callExpression.arguments()) {
152-
if (argument instanceof UnpackingExpression unpacking && "**".equals(unpacking.starToken().value())) {
152+
if (argument instanceof UnpackingExpression unpacking && TreeUtils.isDoubleStarExpression(unpacking)) {
153153
Expression unpackedExpr = resolveExpression(unpacking.expression());
154154
if (unpackedExpr instanceof DictionaryLiteral dictLiteral) {
155155
extractFromSetupConfigDict(dictLiteral);

python-frontend/src/main/java/org/sonar/python/tree/TreeUtils.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import org.sonar.plugins.python.api.tree.Tree;
6363
import org.sonar.plugins.python.api.tree.Tree.Kind;
6464
import org.sonar.plugins.python.api.tree.Tuple;
65+
import org.sonar.plugins.python.api.tree.UnpackingExpression;
6566
import org.sonar.plugins.python.api.types.v2.PythonType;
6667
import org.sonar.python.api.PythonTokenType;
6768

@@ -171,7 +172,7 @@ public static ClassSymbol getClassSymbolFromDef(@Nullable ClassDef classDef) {
171172

172173
@CheckForNull
173174
public static ClassDef getEnclosingClassDef(Tree tree) {
174-
Tree enclosingClass = firstAncestorOfKind(tree, Tree.Kind.CLASSDEF, Tree.Kind.FUNCDEF);
175+
Tree enclosingClass = firstAncestorOfKind(tree, Tree.Kind.CLASSDEF, Tree.Kind.FUNCDEF, Tree.Kind.LAMBDA);
175176
if (enclosingClass instanceof ClassDef classDef) {
176177
return classDef;
177178
}
@@ -634,6 +635,21 @@ public static Set<SymbolV2> getLocalVariableSymbols(FunctionDef functionDef) {
634635
.collect(Collectors.toSet());
635636
}
636637

638+
/**
639+
* Checks if a tree node represents a double-star ({@code **}) expression.
640+
* Supports both {@link Parameter} (e.g. {@code **kwargs} in function definitions)
641+
* and {@link UnpackingExpression} (e.g. {@code **kwargs} in call arguments).
642+
*/
643+
public static boolean isDoubleStarExpression(Tree tree) {
644+
Token starToken = null;
645+
if (tree instanceof Parameter parameter) {
646+
starToken = parameter.starToken();
647+
} else if (tree instanceof UnpackingExpression unpackingExpression) {
648+
starToken = unpackingExpression.starToken();
649+
}
650+
return starToken != null && "**".equals(starToken.value());
651+
}
652+
637653
private static final Pattern CONSTANT_NAME_PATTERN = Pattern.compile("^[_A-Z][A-Z0-9_]*$");
638654

639655
public static boolean isConstantName(String name) {

python-frontend/src/main/resources/org/sonar/python/types/custom_protobuf/django.protobuf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
utils django.utils *
1313
urls django.urls *
1414
conf django.conf *
15-
apps django.apps 
15+
apps django.apps *
16+
views django.views 

python-frontend/src/main/resources/org/sonar/python/types/custom_protobuf/django.views.generic.base.protobuf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
django.views.generic.base�
3+
Viewdjango.views.generic.base.View"builtins.object*`
4+
dispatch'django.views.generic.base.View.dispatch*
5+
self*
6+
request*
7+
args*
8+
9+
kwargs*Y
10+
get_context_data/django.views.generic.base.View.get_context_data*
11+
self*
12+
13+
kwargs�
14+
TemplateView&django.views.generic.base.TemplateView"django.views.generic.base.View*a
15+
get_context_data7django.views.generic.base.TemplateView.get_context_data*
16+
self*
17+
18+
kwargsrc
19+
template_name4django.views.generic.base.TemplateView.template_name
20+
builtins.str" builtins.str*�
21+
__annotations__)django.views.generic.base.__annotations__W
22+
builtins.dict[builtins.str,Any]
23+
builtins.str" builtins.str
24+
Any"builtins.dict

python-frontend/src/main/resources/org/sonar/python/types/custom_protobuf/django.views.generic.detail.protobuf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
django.views.generic.detail�
3+
4+
DetailView&django.views.generic.detail.DetailView"django.views.generic.base.View*a
5+
get_context_data7django.views.generic.detail.DetailView.get_context_data*
6+
self*
7+
8+
kwargsrU
9+
model,django.views.generic.detail.DetailView.model
10+
builtins.type"builtins.type*�
11+
__annotations__+django.views.generic.detail.__annotations__W
12+
builtins.dict[builtins.str,Any]
13+
builtins.str" builtins.str
14+
Any"builtins.dict

python-frontend/src/main/resources/org/sonar/python/types/custom_protobuf/django.views.generic.list.protobuf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
django.views.generic.list�
3+
ListView"django.views.generic.list.ListView"django.views.generic.base.View*]
4+
get_context_data3django.views.generic.list.ListView.get_context_data*
5+
self*
6+
7+
kwargsrQ
8+
model(django.views.generic.list.ListView.model
9+
builtins.type"builtins.type*�
10+
__annotations__)django.views.generic.list.__annotations__W
11+
builtins.dict[builtins.str,Any]
12+
builtins.str" builtins.str
13+
Any"builtins.dict

python-frontend/src/main/resources/org/sonar/python/types/custom_protobuf/django.views.generic.protobuf

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
django.views.generic�
3+
Viewdjango.views.generic.base.View"builtins.object*`
4+
dispatch'django.views.generic.base.View.dispatch*
5+
self*
6+
request*
7+
args*
8+
9+
kwargs*Y
10+
get_context_data/django.views.generic.base.View.get_context_data*
11+
self*
12+
13+
kwargs�
14+
TemplateView&django.views.generic.base.TemplateView"django.views.generic.base.View*a
15+
get_context_data7django.views.generic.base.TemplateView.get_context_data*
16+
self*
17+
18+
kwargsrc
19+
template_name4django.views.generic.base.TemplateView.template_name
20+
builtins.str" builtins.str�
21+
ListView"django.views.generic.list.ListView"django.views.generic.base.View*]
22+
get_context_data3django.views.generic.list.ListView.get_context_data*
23+
self*
24+
25+
kwargsrQ
26+
model(django.views.generic.list.ListView.model
27+
builtins.type"builtins.type�
28+
29+
DetailView&django.views.generic.detail.DetailView"django.views.generic.base.View*a
30+
get_context_data7django.views.generic.detail.DetailView.get_context_data*
31+
self*
32+
33+
kwargsrU
34+
model,django.views.generic.detail.DetailView.model
35+
builtins.type"builtins.type*u
36+
__path__django.views.generic.__path__J
37+
builtins.list[builtins.str]
38+
builtins.str" builtins.str"builtins.list*�
39+
__annotations__$django.views.generic.__annotations__W
40+
builtins.dict[builtins.str,Any]
41+
builtins.str" builtins.str
42+
Any"builtins.dict*#
43+
basedjango.views.generic.base 

python-frontend/src/main/resources/org/sonar/python/types/custom_protobuf/django.views.protobuf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
django.views*m
3+
__path__django.views.__path__J
4+
builtins.list[builtins.str]
5+
builtins.str" builtins.str"builtins.list*�
6+
__annotations__django.views.__annotations__W
7+
builtins.dict[builtins.str,Any]
8+
builtins.str" builtins.str
9+
Any"builtins.dict*!
10+
genericdjango.views.generic 

python-frontend/src/test/java/org/sonar/python/tree/TreeUtilsTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@
4040
import org.sonar.plugins.python.api.tree.IfStatement;
4141
import org.sonar.plugins.python.api.tree.Name;
4242
import org.sonar.plugins.python.api.tree.NumericLiteral;
43+
import org.sonar.plugins.python.api.tree.Parameter;
4344
import org.sonar.plugins.python.api.tree.PassStatement;
4445
import org.sonar.plugins.python.api.tree.QualifiedExpression;
4546
import org.sonar.plugins.python.api.tree.RegularArgument;
47+
import org.sonar.plugins.python.api.tree.UnpackingExpression;
4648
import org.sonar.plugins.python.api.tree.Statement;
4749
import org.sonar.plugins.python.api.tree.Token;
4850
import org.sonar.plugins.python.api.tree.Tree;
@@ -923,6 +925,43 @@ def inner(): pass
923925
assertThat(TreeUtils.getEnclosingClassDef(funcDefInner)).isNull();
924926
}
925927

928+
@Test
929+
void test_isDoubleStarExpression() {
930+
FileInput fileInput = PythonTestUtils.parse("""
931+
def foo(*args, **kwargs): pass
932+
bar(**x, *y)
933+
""");
934+
935+
FunctionDef funcDef = PythonTestUtils.getFirstChild(fileInput, t -> t.is(Kind.FUNCDEF));
936+
Parameter doubleStarParam = TreeUtils.nonTupleParameters(funcDef).stream()
937+
.filter(p -> p.name() != null && "kwargs".equals(p.name().name()))
938+
.findFirst().get();
939+
Parameter singleStarParam = TreeUtils.nonTupleParameters(funcDef).stream()
940+
.filter(p -> p.name() != null && "args".equals(p.name().name()))
941+
.findFirst().get();
942+
943+
assertThat(TreeUtils.isDoubleStarExpression(doubleStarParam)).isTrue();
944+
assertThat(TreeUtils.isDoubleStarExpression(singleStarParam)).isFalse();
945+
946+
CallExpression callExpr = PythonTestUtils.getFirstChild(fileInput, t -> t.is(Kind.CALL_EXPR));
947+
UnpackingExpression doubleStarUnpacking = callExpr.arguments().stream()
948+
.filter(UnpackingExpression.class::isInstance)
949+
.map(UnpackingExpression.class::cast)
950+
.filter(u -> u.expression() instanceof Name name && "x".equals(name.name()))
951+
.findFirst().get();
952+
UnpackingExpression singleStarUnpacking = callExpr.arguments().stream()
953+
.filter(UnpackingExpression.class::isInstance)
954+
.map(UnpackingExpression.class::cast)
955+
.filter(u -> u.expression() instanceof Name name && "y".equals(name.name()))
956+
.findFirst().get();
957+
958+
assertThat(TreeUtils.isDoubleStarExpression(doubleStarUnpacking)).isTrue();
959+
assertThat(TreeUtils.isDoubleStarExpression(singleStarUnpacking)).isFalse();
960+
961+
// A tree that is neither Parameter nor UnpackingExpression
962+
assertThat(TreeUtils.isDoubleStarExpression(funcDef)).isFalse();
963+
}
964+
926965
@Test
927966
void testIsConstantName() {
928967
assertThat(TreeUtils.isConstantName("_FOO")).isTrue();

0 commit comments

Comments
 (0)