Skip to content

Commit 658a3fe

Browse files
SONARPY-914 Match / case statement: support sequence patterns (#983)
1 parent a4f8914 commit 658a3fe

File tree

12 files changed

+414
-18
lines changed

12 files changed

+414
-18
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,4 +505,14 @@ public void visitGuard(Guard guard) {
505505
public void visitCapturePattern(CapturePattern capturePattern) {
506506
scan(capturePattern.name());
507507
}
508+
509+
@Override
510+
public void visitSequencePattern(SequencePattern sequencePattern) {
511+
scan(sequencePattern.elements());
512+
}
513+
514+
@Override
515+
public void visitStarPattern(StarPattern starPattern) {
516+
scan(starPattern.capturePattern());
517+
}
508518
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@
1919
*/
2020
package org.sonar.plugins.python.api.tree;
2121

22+
/**
23+
* Capture Pattern
24+
*
25+
* <pre>
26+
* case x:
27+
* ...
28+
* </pre>
29+
*
30+
* See https://docs.python.org/3/reference/compound_stmts.html#capture-patterns
31+
*/
2232
public interface CapturePattern extends Pattern {
2333

2434
Name name();
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
import java.util.List;
23+
24+
/**
25+
* Sequence Pattern
26+
*
27+
* <pre>
28+
* case [x, y, z]:
29+
* ...
30+
* case (x, y, z):
31+
* ...
32+
* case x, y, z:
33+
* ...
34+
* </pre>
35+
*
36+
* See https://docs.python.org/3/reference/compound_stmts.html#sequence-patterns
37+
*/
38+
public interface SequencePattern extends Pattern {
39+
40+
List<Pattern> elements();
41+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
* Star Pattern
24+
* Only used inside sequence patterns
25+
*
26+
* <pre>
27+
* case [x, y, *others]:
28+
* ...
29+
* </pre>
30+
*
31+
* See https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-star_pattern
32+
*/
33+
public interface StarPattern extends Pattern {
34+
35+
CapturePattern capturePattern();
36+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,14 @@ enum Kind {
138138

139139
RETURN_STMT(ReturnStatement.class),
140140

141+
SEQUENCE_PATTERN(SequencePattern.class),
142+
141143
SET_LITERAL(SetLiteral.class),
142144

143145
STATEMENT_LIST(StatementList.class),
144146

147+
STAR_PATTERN(StarPattern.class),
148+
145149
STRING_LITERAL(StringLiteral.class),
146150

147151
STRING_ELEMENT(StringElement.class),

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,8 @@ public interface TreeVisitor {
184184
void visitGuard(Guard guard);
185185

186186
void visitCapturePattern(CapturePattern capturePattern);
187+
188+
void visitSequencePattern(SequencePattern sequencePattern);
189+
190+
void visitStarPattern(StarPattern starPattern);
187191
}

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,17 @@ public enum PythonGrammar implements GrammarRuleKey {
154154
CASE_BLOCK,
155155
GUARD,
156156

157+
PATTERNS,
157158
PATTERN,
158159
AS_PATTERN,
159160
CLOSED_PATTERN,
160161
LITERAL_PATTERN,
161162
CAPTURE_PATTERN,
163+
SEQUENCE_PATTERN,
164+
STAR_PATTERN,
165+
MAYBE_STAR_PATTERN,
166+
MAYBE_SEQUENCE_PATTERN,
167+
OPEN_SEQUENCE_PATTERN,
162168

163169
SIGNED_NUMBER,
164170
COMPLEX_NUMBER,
@@ -455,14 +461,25 @@ public static void compoundStatements(LexerfulGrammarBuilder b) {
455461

456462
b.rule(MATCH_STMT).is("match", SUBJECT_EXPR, ":", NEWLINE, INDENT, b.oneOrMore(CASE_BLOCK), DEDENT);
457463
b.rule(SUBJECT_EXPR).is(STAR_NAMED_EXPRESSIONS);
458-
b.rule(CASE_BLOCK).is("case", PATTERN, b.optional(GUARD), ":", SUITE);
464+
b.rule(CASE_BLOCK).is("case", PATTERNS, b.optional(GUARD), ":", SUITE);
459465
b.rule(GUARD).is("if", NAMED_EXPR_TEST);
460466

467+
b.rule(PATTERNS).is(b.firstOf(OPEN_SEQUENCE_PATTERN, PATTERN));
461468
b.rule(PATTERN).is(b.firstOf(AS_PATTERN, CLOSED_PATTERN));
462-
b.rule(CLOSED_PATTERN).is(b.firstOf(LITERAL_PATTERN, CAPTURE_PATTERN));
469+
b.rule(CLOSED_PATTERN).is(b.firstOf(LITERAL_PATTERN, CAPTURE_PATTERN, SEQUENCE_PATTERN));
463470
b.rule(AS_PATTERN).is(CLOSED_PATTERN, "as", CAPTURE_PATTERN);
464471
b.rule(CAPTURE_PATTERN).is(NAME);
465472

473+
b.rule(SEQUENCE_PATTERN).is(b.firstOf(
474+
b.sequence("[", b.optional(MAYBE_SEQUENCE_PATTERN) , "]"),
475+
b.sequence("(", b.optional(OPEN_SEQUENCE_PATTERN), ")")
476+
));
477+
b.rule(OPEN_SEQUENCE_PATTERN).is(MAYBE_STAR_PATTERN, ",", b.optional(MAYBE_SEQUENCE_PATTERN));
478+
b.rule(MAYBE_SEQUENCE_PATTERN).is(MAYBE_STAR_PATTERN, b.zeroOrMore(",", MAYBE_STAR_PATTERN), b.optional(","));
479+
b.rule(MAYBE_STAR_PATTERN).is(b.firstOf(STAR_PATTERN, PATTERN));
480+
// TODO: ADD wildcard pattern for "*_"
481+
b.rule(STAR_PATTERN).is("*", CAPTURE_PATTERN);
482+
466483
b.rule(LITERAL_PATTERN).is(b.firstOf(
467484
COMPLEX_NUMBER,
468485
SIGNED_NUMBER,

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

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@
8181
import org.sonar.plugins.python.api.tree.RaiseStatement;
8282
import org.sonar.plugins.python.api.tree.RegularArgument;
8383
import org.sonar.plugins.python.api.tree.ReturnStatement;
84+
import org.sonar.plugins.python.api.tree.SequencePattern;
8485
import org.sonar.plugins.python.api.tree.SliceItem;
8586
import org.sonar.plugins.python.api.tree.SliceList;
87+
import org.sonar.plugins.python.api.tree.StarPattern;
8688
import org.sonar.plugins.python.api.tree.Statement;
8789
import org.sonar.plugins.python.api.tree.StatementList;
8890
import org.sonar.plugins.python.api.tree.StringElement;
@@ -805,7 +807,8 @@ public MatchStatement matchStatement(AstNode matchStmt) {
805807

806808
public CaseBlock caseBlock(AstNode caseBlock) {
807809
Token caseKeyword = toPyToken(caseBlock.getTokens().get(0));
808-
Pattern pattern = pattern(caseBlock.getFirstChild(PythonGrammar.PATTERN).getFirstChild());
810+
AstNode patternOrSequence = caseBlock.getFirstChild(PythonGrammar.PATTERNS).getFirstChild();
811+
Pattern pattern = patternOrSequence.is(PythonGrammar.PATTERN) ? pattern(patternOrSequence.getFirstChild()) : sequencePattern(patternOrSequence);
809812
Guard guard = null;
810813
AstNode guardNode = caseBlock.getFirstChild(PythonGrammar.GUARD);
811814
if (guardNode != null) {
@@ -843,8 +846,62 @@ private static Pattern closedPattern(AstNode closedPattern) {
843846
return literalPattern(astNode);
844847
} else if (astNode.is(PythonGrammar.CAPTURE_PATTERN)) {
845848
return new CapturePatternImpl(name(astNode.getFirstChild()));
849+
} else if (astNode.is(PythonGrammar.SEQUENCE_PATTERN)) {
850+
return sequencePattern(astNode);
851+
}
852+
throw new IllegalStateException(String.format("Pattern %s not recognized.", astNode.getName()));
853+
}
854+
855+
private static SequencePattern sequencePattern(AstNode sequencePattern) {
856+
AstNode leftDelimiter = sequencePattern.getFirstChild(PythonPunctuator.LPARENTHESIS, PythonPunctuator.LBRACKET);
857+
AstNode rightDelimiter = sequencePattern.getFirstChild(PythonPunctuator.RPARENTHESIS, PythonPunctuator.RBRACKET);
858+
List<Token> commas = new ArrayList<>();
859+
List<Pattern> patterns = new ArrayList<>();
860+
if (leftDelimiter == null) {
861+
// sequence patterns without neither parenthesis nor square brackets.
862+
// In this case there needs to be at least one comma
863+
addPatternsAndCommasFromSequencePattern(sequencePattern, commas, patterns);
864+
return new SequencePatternImpl(null, patterns, commas, null);
865+
}
866+
if (leftDelimiter.is(PythonPunctuator.LPARENTHESIS)) {
867+
// we need to treat differently when delimiters are parenthesis '(' ')' because there needs to be at least one comma
868+
// e.g. '(x)' is not a sequence pattern but a group pattern instead, while '(x,)' is a sequence pattern
869+
AstNode openSequencePattern = sequencePattern.getFirstChild(PythonGrammar.OPEN_SEQUENCE_PATTERN);
870+
if (openSequencePattern != null) {
871+
addPatternsAndCommasFromSequencePattern(openSequencePattern, commas, patterns);
872+
}
873+
} else {
874+
addPatternsAndCommasFromMaybeSequencePattern(sequencePattern.getFirstChild(PythonGrammar.MAYBE_SEQUENCE_PATTERN), patterns, commas);
875+
}
876+
return new SequencePatternImpl(toPyToken(leftDelimiter.getToken()), patterns, commas, toPyToken(rightDelimiter.getToken()));
877+
}
878+
879+
private static void addPatternsAndCommasFromSequencePattern(AstNode sequencePattern, List<Token> commas, List<Pattern> patterns) {
880+
commas.add(toPyToken(sequencePattern.getFirstChild(PythonPunctuator.COMMA).getToken()));
881+
patterns.add(maybeStarPattern(sequencePattern.getFirstChild(PythonGrammar.MAYBE_STAR_PATTERN)));
882+
addPatternsAndCommasFromMaybeSequencePattern(sequencePattern.getFirstChild(PythonGrammar.MAYBE_SEQUENCE_PATTERN), patterns, commas);
883+
}
884+
885+
private static void addPatternsAndCommasFromMaybeSequencePattern(@Nullable AstNode maybeSequencePattern,List<Pattern> patterns, List<Token> commas) {
886+
if (maybeSequencePattern == null) {
887+
return;
888+
}
889+
patterns.addAll(maybeSequencePattern.getChildren(PythonGrammar.MAYBE_STAR_PATTERN).stream().map(PythonTreeMaker::maybeStarPattern).collect(Collectors.toList()));
890+
commas.addAll(punctuators(maybeSequencePattern, PythonPunctuator.COMMA));
891+
}
892+
893+
private static Pattern maybeStarPattern(AstNode maybeStarPattern) {
894+
AstNode astNode = maybeStarPattern.getFirstChild();
895+
if (astNode.is(PythonGrammar.STAR_PATTERN)) {
896+
return starPattern(astNode);
846897
}
847-
throw new IllegalStateException(String.format("Pattern %s not recognized.", closedPattern.getName()));
898+
return pattern(astNode.getFirstChild());
899+
}
900+
901+
private static StarPattern starPattern(AstNode starPattern) {
902+
Token starToken = toPyToken(starPattern.getFirstChild(PythonPunctuator.MUL).getToken());
903+
AstNode capturePattern = starPattern.getFirstChild(PythonGrammar.CAPTURE_PATTERN);
904+
return new StarPatternImpl(starToken, new CapturePatternImpl(name(capturePattern.getFirstChild())));
848905
}
849906

850907
private static LiteralPattern literalPattern(AstNode literalPattern) {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.tree;
21+
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import javax.annotation.Nullable;
25+
import org.sonar.plugins.python.api.tree.Pattern;
26+
import org.sonar.plugins.python.api.tree.SequencePattern;
27+
import org.sonar.plugins.python.api.tree.Token;
28+
import org.sonar.plugins.python.api.tree.Tree;
29+
import org.sonar.plugins.python.api.tree.TreeVisitor;
30+
31+
public class SequencePatternImpl extends PyTree implements SequencePattern {
32+
33+
@Nullable
34+
private final Token leftDelimiter; // might be '(' or '['
35+
private final List<Pattern> elements;
36+
private final List<Token> commas;
37+
@Nullable
38+
private final Token rightDelimiter; // might be ')' or ']'
39+
40+
public SequencePatternImpl(@Nullable Token leftDelimiter, List<Pattern> elements, List<Token> commas, @Nullable Token rightDelimiter) {
41+
this.leftDelimiter = leftDelimiter;
42+
this.elements = elements;
43+
this.commas = commas;
44+
this.rightDelimiter = rightDelimiter;
45+
}
46+
47+
@Override
48+
public List<Pattern> elements() {
49+
return elements;
50+
}
51+
52+
@Override
53+
public void accept(TreeVisitor visitor) {
54+
visitor.visitSequencePattern(this);
55+
}
56+
57+
@Override
58+
public Kind getKind() {
59+
return Kind.SEQUENCE_PATTERN;
60+
}
61+
62+
@Override
63+
List<Tree> computeChildren() {
64+
List<Tree> children = new ArrayList<>();
65+
if (leftDelimiter != null) {
66+
children.add(leftDelimiter);
67+
}
68+
int i = 0;
69+
for (Pattern element : elements) {
70+
children.add(element);
71+
if (i < commas.size()) {
72+
children.add(commas.get(i));
73+
}
74+
i++;
75+
}
76+
if (rightDelimiter != null) {
77+
children.add(rightDelimiter);
78+
}
79+
return children;
80+
}
81+
}

0 commit comments

Comments
 (0)