Skip to content

Commit 8a6a4a7

Browse files
SONARPY-909 Add name bindings for capture patterns (#996)
1 parent d0e8a7f commit 8a6a4a7

File tree

12 files changed

+227
-19
lines changed

12 files changed

+227
-19
lines changed

python-checks/src/test/resources/checks/deadStore.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,6 @@ def match_statement_no_fp(value):
316316
a = MyClass()
317317
match value:
318318
case 1: ...
319-
case a.CONST: a = 42 # OK, read before write
319+
case a.CONST:
320+
a = 42
321+
print(a)

python-checks/src/test/resources/checks/ignoredParameter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ def assignment_expression_fn(a): # FN (first dict key computation overwrites "a"
5151
def assignment_expression_no_fp(a):
5252
dict = {'b' : a, 'c' : (a:=3)} # OK, read before write
5353

54-
def match_statement_fp(value, param): # Noncompliant
54+
def match_statement_no_fp(value, param):
5555
match value:
56-
case param.CONST: param = 42 # FP here: b.CONST should be a reading usage of b
56+
case param.CONST: param = 42
5757
case "other": ...
5858
value = 42 # OK
5959

60-
def match_statement_fn(value, param):
60+
def match_statement_no_fn(value, param): # Noncompliant
6161
match value:
6262
case 1: ...
63-
case param: ... # FN, c is overridden without having been read
63+
case param: ...
6464
value = 42 # OK

python-checks/src/test/resources/checks/undefinedSymbols/undefinedSymbols.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,9 @@ def use_glob3():
105105

106106
_some_implicit_global_vars # ok we exclude variables starting with `_`
107107
__some_implicit_global_vars__ # ok
108+
109+
110+
def python_3_10(value):
111+
match value:
112+
case remaining:
113+
print(remaining)

python-frontend/src/main/java/org/sonar/plugins/python/api/symbols/Usage.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ enum Kind {
4343
CLASS_DECLARATION,
4444
EXCEPTION_INSTANCE,
4545
WITH_INSTANCE,
46-
GLOBAL_DECLARATION
46+
GLOBAL_DECLARATION,
47+
PATTERN_DECLARATION
4748
}
4849
}

python-frontend/src/main/java/org/sonar/plugins/python/api/tree/AsPattern.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ public interface AsPattern extends Pattern {
3333

3434
Token asKeyword();
3535

36-
Name alias();
36+
CapturePattern alias();
3737
}

python-frontend/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@
4646
import org.sonar.plugins.python.api.tree.AssignmentExpression;
4747
import org.sonar.plugins.python.api.tree.AssignmentStatement;
4848
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
49+
import org.sonar.plugins.python.api.tree.CapturePattern;
4950
import org.sonar.plugins.python.api.tree.ClassDef;
5051
import org.sonar.plugins.python.api.tree.CompoundAssignmentStatement;
5152
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
5253
import org.sonar.plugins.python.api.tree.ComprehensionFor;
53-
import org.sonar.plugins.python.api.tree.Decorator;
5454
import org.sonar.plugins.python.api.tree.DottedName;
5555
import org.sonar.plugins.python.api.tree.ExceptClause;
5656
import org.sonar.plugins.python.api.tree.Expression;
@@ -541,6 +541,12 @@ public void visitWithItem(WithItem withItem) {
541541
super.visitWithItem(withItem);
542542
}
543543

544+
@Override
545+
public void visitCapturePattern(CapturePattern capturePattern) {
546+
addBindingUsage(capturePattern.name(), Usage.Kind.PATTERN_DECLARATION);
547+
super.visitCapturePattern(capturePattern);
548+
}
549+
544550
private void createScope(Tree tree, @Nullable Scope parent) {
545551
scopesByRootTree.put(tree, new Scope(parent, tree, pythonFile, fullyQualifiedModuleName, projectLevelSymbolTable));
546552
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import java.util.Arrays;
2323
import java.util.List;
2424
import org.sonar.plugins.python.api.tree.AsPattern;
25-
import org.sonar.plugins.python.api.tree.Name;
25+
import org.sonar.plugins.python.api.tree.CapturePattern;
2626
import org.sonar.plugins.python.api.tree.Pattern;
2727
import org.sonar.plugins.python.api.tree.Token;
2828
import org.sonar.plugins.python.api.tree.Tree;
@@ -31,9 +31,9 @@
3131
public class AsPatternImpl extends PyTree implements AsPattern {
3232
private final Pattern pattern;
3333
private final Token asKeyword;
34-
private final Name alias;
34+
private final CapturePattern alias;
3535

36-
public AsPatternImpl(Pattern pattern, Token asKeyword, Name alias) {
36+
public AsPatternImpl(Pattern pattern, Token asKeyword, CapturePattern alias) {
3737
this.pattern = pattern;
3838
this.asKeyword = asKeyword;
3939
this.alias = alias;
@@ -50,7 +50,7 @@ public Token asKeyword() {
5050
}
5151

5252
@Override
53-
public Name alias() {
53+
public CapturePattern alias() {
5454
return alias;
5555
}
5656

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ private static Pattern orPattern(AstNode pattern) {
857857
private static AsPattern asPattern(AstNode asPattern) {
858858
Pattern pattern = orPattern(asPattern.getFirstChild(PythonGrammar.OR_PATTERN));
859859
Token asKeyword = toPyToken(asPattern.getFirstChild(PythonKeyword.AS).getToken());
860-
Name alias = name(asPattern.getFirstChild(PythonGrammar.CAPTURE_PATTERN).getFirstChild());
860+
CapturePattern alias = new CapturePatternImpl(name(asPattern.getFirstChild(PythonGrammar.CAPTURE_PATTERN).getFirstChild()));
861861
return new AsPatternImpl(pattern, asKeyword, alias);
862862
}
863863

@@ -938,9 +938,9 @@ private static Expression nameOrAttr(AstNode nameOrAttr) {
938938
List<Token> dots = punctuators(nameOrAttr, PythonPunctuator.DOT);
939939
List<AstNode> names = nameOrAttr.getChildren(PythonGrammar.NAME);
940940
if (dots.isEmpty()) {
941-
return name(names.get(0));
941+
return variable(names.get(0));
942942
}
943-
Expression qualifier = name(names.get(0));
943+
Expression qualifier = variable(names.get(0));
944944

945945
for (int i = 1; i < names.size(); i++) {
946946
Name name = name(names.get(i));

python-frontend/src/test/java/org/sonar/plugins/python/api/tree/BaseTreeVisitorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ public void as_pattern() {
409409
pattern.accept(visitor);
410410

411411
verify(visitor).visitLiteralPattern((LiteralPattern) pattern.pattern());
412-
verify(visitor).visitName(pattern.alias());
412+
verify(visitor).visitCapturePattern(pattern.alias());
413413
}
414414

415415
@Test
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2021 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.semantic;
21+
22+
import org.junit.Test;
23+
import org.sonar.plugins.python.api.symbols.Symbol;
24+
import org.sonar.plugins.python.api.symbols.Usage;
25+
import org.sonar.plugins.python.api.tree.AsPattern;
26+
import org.sonar.plugins.python.api.tree.CapturePattern;
27+
import org.sonar.plugins.python.api.tree.CaseBlock;
28+
import org.sonar.plugins.python.api.tree.ClassPattern;
29+
import org.sonar.plugins.python.api.tree.DoubleStarPattern;
30+
import org.sonar.plugins.python.api.tree.GroupPattern;
31+
import org.sonar.plugins.python.api.tree.KeyValuePattern;
32+
import org.sonar.plugins.python.api.tree.KeywordPattern;
33+
import org.sonar.plugins.python.api.tree.MappingPattern;
34+
import org.sonar.plugins.python.api.tree.Name;
35+
import org.sonar.plugins.python.api.tree.OrPattern;
36+
import org.sonar.plugins.python.api.tree.Pattern;
37+
import org.sonar.plugins.python.api.tree.QualifiedExpression;
38+
import org.sonar.plugins.python.api.tree.SequencePattern;
39+
import org.sonar.plugins.python.api.tree.StarPattern;
40+
import org.sonar.plugins.python.api.tree.ValuePattern;
41+
import org.sonar.python.PythonTestUtils;
42+
43+
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.sonar.plugins.python.api.symbols.Usage.Kind.CLASS_DECLARATION;
45+
import static org.sonar.plugins.python.api.symbols.Usage.Kind.OTHER;
46+
import static org.sonar.plugins.python.api.symbols.Usage.Kind.PATTERN_DECLARATION;
47+
import static org.sonar.plugins.python.api.tree.Tree.Kind.CASE_BLOCK;
48+
import static org.sonar.plugins.python.api.tree.Tree.Kind.NAME;
49+
import static org.sonar.python.PythonTestUtils.getLastDescendant;
50+
51+
public class MatchStatementSymbolsTest {
52+
53+
@Test
54+
public void capture_pattern() {
55+
CapturePattern capturePattern = patternFromCase("case others: print(others)");
56+
Symbol others = capturePattern.name().symbol();
57+
assertThat(others.name()).isEqualTo("others");
58+
assertThat(others.fullyQualifiedName()).isNull();
59+
assertThat(others.usages()).extracting(Usage::kind).containsExactly(PATTERN_DECLARATION, OTHER);
60+
}
61+
62+
@Test
63+
public void wildcard_pattern() {
64+
Name wildcard = getLastDescendant(PythonTestUtils.parse(
65+
"def foo(value):",
66+
" match(value):",
67+
" case _: print(_)"
68+
), t -> t.is(NAME) && ((Name) t).name().equals("_"));
69+
assertThat(wildcard.symbol()).isNull();
70+
}
71+
72+
@Test
73+
public void class_pattern() {
74+
ClassPattern classPattern = pattern(
75+
"class A:",
76+
" foo = 42",
77+
"def foo(value):",
78+
" match(value):",
79+
" case A(foo=x):",
80+
" print(x)");
81+
Name name = (Name) classPattern.targetClass();
82+
assertThat(name.symbol().kind()).isEqualTo(Symbol.Kind.CLASS);
83+
KeywordPattern keywordPattern = ((KeywordPattern) classPattern.arguments().get(0));
84+
Symbol x = ((CapturePattern) keywordPattern.pattern()).name().symbol();
85+
assertThat(x.usages()).extracting(Usage::kind).containsExactly(PATTERN_DECLARATION, OTHER);
86+
87+
classPattern = pattern(
88+
"import mod",
89+
"def foo(value):",
90+
" match(value):",
91+
" case mod.A(foo=x):",
92+
" print(x)");
93+
QualifiedExpression qualifiedExpression = (QualifiedExpression) classPattern.targetClass();
94+
assertThat(qualifiedExpression.symbol().kind()).isEqualTo(Symbol.Kind.OTHER);
95+
}
96+
97+
@Test
98+
public void value_pattern() {
99+
ValuePattern valuePattern = pattern(
100+
"import command",
101+
"def foo(value):",
102+
" match(value):",
103+
" case command.QUIT:",
104+
" print(x)");
105+
assertThat(valuePattern.qualifiedExpression().symbol().kind()).isEqualTo(Symbol.Kind.OTHER);
106+
}
107+
108+
@Test
109+
public void as_pattern() {
110+
AsPattern asPattern = patternFromCase("case 42 as x: ...");
111+
Symbol symbol = asPattern.alias().name().symbol();
112+
assertThat(symbol.name()).isEqualTo("x");
113+
assertThat(symbol.fullyQualifiedName()).isNull();
114+
assertThat(symbol.usages()).extracting(Usage::kind).containsExactly(PATTERN_DECLARATION);
115+
116+
asPattern = patternFromCase("case z as x: return x + z");
117+
Symbol z = ((CapturePattern) asPattern.pattern()).name().symbol();
118+
Symbol x = asPattern.alias().name().symbol();
119+
assertThat(z.name()).isEqualTo("z");
120+
assertThat(x.name()).isEqualTo("x");
121+
assertThat(z.usages()).extracting(Usage::kind).containsExactly(PATTERN_DECLARATION, OTHER);
122+
assertThat(x.usages()).extracting(Usage::kind).containsExactly(PATTERN_DECLARATION, OTHER);
123+
}
124+
125+
@Test
126+
public void or_pattern() {
127+
OrPattern orPattern = pattern(
128+
"class A: ...",
129+
"class B: ...",
130+
"def foo(value):",
131+
" match(value):",
132+
" case A() | B(): ...");
133+
Symbol symbolA = ((Name) ((ClassPattern) orPattern.patterns().get(0)).targetClass()).symbol();
134+
Symbol symbolB = ((Name) ((ClassPattern) orPattern.patterns().get(1)).targetClass()).symbol();
135+
assertThat(symbolA.usages()).extracting(Usage::kind).containsExactly(CLASS_DECLARATION, OTHER);
136+
assertThat(symbolB.usages()).extracting(Usage::kind).containsExactly(CLASS_DECLARATION, OTHER);
137+
}
138+
139+
@Test
140+
public void sequence_pattern() {
141+
SequencePattern sequencePattern = patternFromCase("case [42, x, *others]: ...");
142+
CapturePattern capturePattern = (CapturePattern) sequencePattern.elements().get(1);
143+
StarPattern starPattern = (StarPattern) sequencePattern.elements().get(2);
144+
assertThat(capturePattern.name().symbol()).isNotNull();
145+
assertThat(((CapturePattern) starPattern.pattern()).name().symbol()).isNotNull();
146+
}
147+
148+
@Test
149+
public void mapping_pattern() {
150+
MappingPattern mappingPattern = patternFromCase("case {'x': 'foo', 'y': val, **others}: ...");
151+
KeyValuePattern keyValuePattern = (KeyValuePattern) mappingPattern.elements().get(1);
152+
CapturePattern capturePattern = (CapturePattern) keyValuePattern.value();
153+
DoubleStarPattern doubleStarPattern = (DoubleStarPattern) mappingPattern.elements().get(2);
154+
assertThat(capturePattern.name().symbol()).isNotNull();
155+
assertThat(doubleStarPattern.capturePattern().name().symbol()).isNotNull();
156+
}
157+
158+
@Test
159+
public void group_pattern() {
160+
GroupPattern groupPattern = patternFromCase("case (x): ...");
161+
CapturePattern capturePattern = (CapturePattern) groupPattern.pattern();
162+
assertThat(capturePattern.name().symbol()).isNotNull();
163+
}
164+
165+
@Test
166+
public void guard() {
167+
CapturePattern capturePattern = patternFromCase("case x if x > 42: ...");
168+
assertThat(capturePattern.name().symbol().usages()).extracting(Usage::kind).containsExactly(PATTERN_DECLARATION, OTHER);
169+
}
170+
171+
@SuppressWarnings("unchecked")
172+
private static <T extends Pattern> T pattern(String... lines) {
173+
String code = String.join(System.getProperty("line.separator"), lines);
174+
CaseBlock caseBlock = getLastDescendant(PythonTestUtils.parse(code), t -> t.is(CASE_BLOCK));
175+
return ((T) caseBlock.pattern());
176+
}
177+
178+
@SuppressWarnings("unchecked")
179+
private static <T extends Pattern> T patternFromCase(String code) {
180+
CaseBlock caseBlock = getLastDescendant(PythonTestUtils.parse(
181+
"def foo(value):",
182+
" match(value):",
183+
" " + code
184+
), t -> t.is(CASE_BLOCK));
185+
return (T) caseBlock.pattern();
186+
}
187+
}

0 commit comments

Comments
 (0)