Skip to content

Commit 33db918

Browse files
SONARPY-908 Basic support of match / case statement (#969)
1 parent d512427 commit 33db918

File tree

21 files changed

+1050
-26
lines changed

21 files changed

+1050
-26
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,16 @@ def type_hints_should_be_excluded():
103103
a: "httpx.Response"
104104
b: "httpx.Response"
105105
c: "httpx.Response"
106+
107+
def literal_patterns_should_be_excluded(value1, value2, value3):
108+
match value1:
109+
case "This is a duplicated pattern":
110+
print("This is a duplicated literal") # Noncompliant
111+
112+
match value2:
113+
case "This is a duplicated pattern":
114+
print("This is a duplicated literal")
115+
116+
match value3:
117+
case "This is a duplicated pattern":
118+
print("This is a duplicated literal")

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,4 +471,27 @@ public void visitFormattedExpression(FormattedExpression formattedExpression) {
471471
public void visitFormatSpecifier(FormatSpecifier formatSpecifier) {
472472
scan(formatSpecifier.formatExpressions());
473473
}
474+
475+
@Override
476+
public void visitMatchStatement(MatchStatement matchStatement) {
477+
scan(matchStatement.subjectExpression());
478+
scan(matchStatement.caseBlocks());
479+
}
480+
481+
@Override
482+
public void visitCaseBlock(CaseBlock caseBlock) {
483+
scan(caseBlock.patterns());
484+
scan(caseBlock.guard());
485+
scan(caseBlock.body());
486+
}
487+
488+
@Override
489+
public void visitLiteralPattern(LiteralPattern literalPattern) {
490+
// noop
491+
}
492+
493+
@Override
494+
public void visitGuard(Guard guard) {
495+
scan(guard.condition());
496+
}
474497
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.plugins.python.api.tree;
21+
22+
23+
import java.util.List;
24+
import javax.annotation.CheckForNull;
25+
26+
/**
27+
* Case block
28+
*
29+
* <pre>
30+
* {@link #caseKeyword()} {@link #patterns()} {@link #guard()}:
31+
* {@link #body()}
32+
* </pre>
33+
*
34+
* See https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-case_block
35+
*/
36+
public interface CaseBlock extends Tree {
37+
38+
Token caseKeyword();
39+
40+
List<Pattern> patterns();
41+
42+
@CheckForNull
43+
Guard guard();
44+
45+
StatementList body();
46+
47+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.plugins.python.api.tree;
21+
22+
/**
23+
* Guard
24+
*
25+
* <pre> {@link #ifKeyword()} {@link #condition()}</pre>
26+
*
27+
* See https://docs.python.org/3/reference/compound_stmts.html#guards
28+
*/
29+
public interface Guard extends Tree {
30+
31+
Token ifKeyword();
32+
33+
Expression condition();
34+
35+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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.plugins.python.api.tree;
21+
22+
/**
23+
* Literal Pattern
24+
*
25+
* <pre>
26+
* case "foo":
27+
* ...
28+
* case 42:
29+
* ...
30+
* case True:
31+
* ...
32+
* case None:
33+
* ...
34+
* </pre>
35+
*
36+
* See https://docs.python.org/3/reference/compound_stmts.html#literal-patterns
37+
*/
38+
public interface LiteralPattern extends Pattern {
39+
40+
String valueAsString();
41+
42+
LiteralKind literalKind();
43+
44+
enum LiteralKind {
45+
STRING,
46+
NUMBER,
47+
BOOLEAN,
48+
NONE
49+
}
50+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.plugins.python.api.tree;
21+
22+
23+
import java.util.List;
24+
25+
/**
26+
* Match statement
27+
*
28+
* <pre>
29+
* {@link #matchKeyword()} {@link #subjectExpression()} :
30+
* {@link #caseBlocks()}
31+
* </pre>
32+
*
33+
* See https://docs.python.org/3/reference/compound_stmts.html#the-match-statement
34+
*/
35+
public interface MatchStatement extends Statement {
36+
37+
Token matchKeyword();
38+
39+
Expression subjectExpression();
40+
41+
List<CaseBlock> caseBlocks();
42+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.plugins.python.api.tree;
21+
22+
public interface Pattern extends Tree {
23+
24+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ enum Kind {
5656

5757
CALL_EXPR(CallExpression.class),
5858

59+
CASE_BLOCK(CaseBlock.class),
60+
5961
CLASSDEF(ClassDef.class),
6062

6163
CONDITIONAL_EXPR(ConditionalExpression.class),
@@ -94,6 +96,8 @@ enum Kind {
9496

9597
GLOBAL_STMT(GlobalStatement.class),
9698

99+
GUARD(Guard.class),
100+
97101
IF_STMT(IfStatement.class),
98102

99103
IMPORT_FROM(ImportFrom.class),
@@ -106,6 +110,10 @@ enum Kind {
106110

107111
LIST_LITERAL(ListLiteral.class),
108112

113+
LITERAL_PATTERN(LiteralPattern.class),
114+
115+
MATCH_STMT(MatchStatement.class),
116+
109117
NAME(Name.class),
110118

111119
NONLOCAL_STMT(NonlocalStatement.class),

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,12 @@ public interface TreeVisitor {
172172
void visitFormattedExpression(FormattedExpression formattedExpression);
173173

174174
void visitFormatSpecifier(FormatSpecifier formatSpecifier);
175+
176+
void visitMatchStatement(MatchStatement matchStatement);
177+
178+
void visitCaseBlock(CaseBlock caseBlock);
179+
180+
void visitLiteralPattern(LiteralPattern literalPattern);
181+
182+
void visitGuard(Guard guard);
175183
}

python-frontend/src/main/java/org/sonar/python/api/PythonGrammar.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import static org.sonar.python.api.PythonTokenType.DEDENT;
3030
import static org.sonar.python.api.PythonTokenType.INDENT;
3131
import static org.sonar.python.api.PythonTokenType.NEWLINE;
32+
import static org.sonar.python.api.PythonTokenType.NUMBER;
3233

3334
public enum PythonGrammar implements GrammarRuleKey {
3435
FACTOR,
@@ -83,6 +84,8 @@ public enum PythonGrammar implements GrammarRuleKey {
8384
OR_EXPR,
8485

8586
NAMED_EXPR_TEST,
87+
STAR_NAMED_EXPRESSIONS,
88+
STAR_NAMED_EXPRESSION,
8689
FORMATTED_EXPR,
8790
F_STRING_CONTENT,
8891
FORMAT_SPECIFIER,
@@ -146,6 +149,17 @@ public enum PythonGrammar implements GrammarRuleKey {
146149
WITH_STMT,
147150
WITH_ITEM,
148151

152+
MATCH_STMT,
153+
SUBJECT_EXPR,
154+
CASE_BLOCK,
155+
GUARD,
156+
157+
PATTERNS,
158+
LITERAL_PATTERN,
159+
160+
SIGNED_NUMBER,
161+
COMPLEX_NUMBER,
162+
149163
FUNCDEF,
150164
DECORATORS,
151165
DECORATOR,
@@ -193,6 +207,11 @@ public static void grammar(LexerfulGrammarBuilder b) {
193207
b.rule(AUGASSIGN).is(b.firstOf("+=", "-=", "*=", "/=", "//=", "%=", "**=", ">>=", "<<=", "&=", "^=", "|=", "@="));
194208

195209
b.rule(NAMED_EXPR_TEST).is(TEST, b.optional(PythonPunctuator.WALRUS_OPERATOR, TEST));
210+
b.rule(STAR_NAMED_EXPRESSIONS).is(STAR_NAMED_EXPRESSION, b.zeroOrMore(",", STAR_NAMED_EXPRESSION), b.optional(","));
211+
b.rule(STAR_NAMED_EXPRESSION).is(b.firstOf(
212+
STAR_EXPR,
213+
NAMED_EXPR_TEST
214+
));
196215
b.rule(TEST).is(b.firstOf(
197216
b.sequence(OR_TEST, b.optional("if", OR_TEST, "else", TEST)),
198217
LAMBDEF));
@@ -401,6 +420,7 @@ public static void compoundStatements(LexerfulGrammarBuilder b) {
401420
FOR_STMT,
402421
TRY_STMT,
403422
WITH_STMT,
423+
MATCH_STMT,
404424
FUNCDEF,
405425
CLASSDEF,
406426
ASYNC_STMT));
@@ -430,6 +450,33 @@ public static void compoundStatements(LexerfulGrammarBuilder b) {
430450
));
431451
b.rule(WITH_ITEM).is(TEST, b.optional("as", EXPR));
432452

453+
b.rule(MATCH_STMT).is("match", SUBJECT_EXPR, ":", NEWLINE, INDENT, b.oneOrMore(CASE_BLOCK), DEDENT);
454+
b.rule(SUBJECT_EXPR).is(STAR_NAMED_EXPRESSIONS);
455+
b.rule(CASE_BLOCK).is("case", PATTERNS, b.optional(GUARD), ":", SUITE);
456+
b.rule(GUARD).is("if", NAMED_EXPR_TEST);
457+
458+
// TODO: this should be either OR_PATTERN or AS_PATTERN
459+
b.rule(PATTERNS).is(LITERAL_PATTERN);
460+
461+
b.rule(LITERAL_PATTERN).is(b.firstOf(
462+
COMPLEX_NUMBER,
463+
SIGNED_NUMBER,
464+
b.oneOrMore(PythonTokenType.STRING),
465+
PythonKeyword.NONE,
466+
"True",
467+
"False"
468+
));
469+
470+
b.rule(COMPLEX_NUMBER).is(b.firstOf(
471+
b.sequence(SIGNED_NUMBER, "+" , NUMBER),
472+
b.sequence(SIGNED_NUMBER, "-", NUMBER))
473+
);
474+
475+
b.rule(SIGNED_NUMBER).is(b.firstOf(
476+
NUMBER,
477+
b.sequence("-", NUMBER)
478+
));
479+
433480
b.rule(FUNCDEF).is(b.optional(DECORATORS), b.optional("async"), "def", FUNCNAME, "(", b.optional(TYPEDARGSLIST), ")", b.optional(FUN_RETURN_ANNOTATION), ":", SUITE);
434481
b.rule(FUNCNAME).is(NAME);
435482
b.rule(FUN_RETURN_ANNOTATION).is("-", ">", TEST);

0 commit comments

Comments
 (0)