Skip to content

Commit 54e74cf

Browse files
authored
SONARPY-919 Match / case statement: OR patterns (#986)
1 parent 658a3fe commit 54e74cf

File tree

9 files changed

+166
-9
lines changed

9 files changed

+166
-9
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,11 @@ public void visitAsPattern(AsPattern asPattern) {
496496
scan(asPattern.alias());
497497
}
498498

499+
@Override
500+
public void visitOrPattern(OrPattern orPattern) {
501+
scan(orPattern.patterns());
502+
}
503+
499504
@Override
500505
public void visitGuard(Guard guard) {
501506
scan(guard.condition());
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
* OR pattern
26+
*
27+
* <pre>
28+
* case "foo" | "bar": ...
29+
* </pre>
30+
*
31+
* See https://docs.python.org/3/reference/compound_stmts.html#or-patterns
32+
*/
33+
public interface OrPattern extends Pattern {
34+
35+
List<Pattern> patterns();
36+
37+
List<Token> separators();
38+
39+
}

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

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

127127
NUMERIC_LITERAL(NumericLiteral.class),
128128

129+
OR_PATTERN(OrPattern.class),
130+
129131
PASS_STMT(PassStatement.class),
130132

131133
PRINT_STMT(PrintStatement.class),

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ public interface TreeVisitor {
181181

182182
void visitAsPattern(AsPattern asPattern);
183183

184+
void visitOrPattern(OrPattern orPattern);
185+
184186
void visitGuard(Guard guard);
185187

186188
void visitCapturePattern(CapturePattern capturePattern);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ public enum PythonGrammar implements GrammarRuleKey {
157157
PATTERNS,
158158
PATTERN,
159159
AS_PATTERN,
160+
OR_PATTERN,
160161
CLOSED_PATTERN,
161162
LITERAL_PATTERN,
162163
CAPTURE_PATTERN,
@@ -465,9 +466,10 @@ public static void compoundStatements(LexerfulGrammarBuilder b) {
465466
b.rule(GUARD).is("if", NAMED_EXPR_TEST);
466467

467468
b.rule(PATTERNS).is(b.firstOf(OPEN_SEQUENCE_PATTERN, PATTERN));
468-
b.rule(PATTERN).is(b.firstOf(AS_PATTERN, CLOSED_PATTERN));
469+
b.rule(PATTERN).is(b.firstOf(AS_PATTERN, OR_PATTERN));
469470
b.rule(CLOSED_PATTERN).is(b.firstOf(LITERAL_PATTERN, CAPTURE_PATTERN, SEQUENCE_PATTERN));
470-
b.rule(AS_PATTERN).is(CLOSED_PATTERN, "as", CAPTURE_PATTERN);
471+
b.rule(AS_PATTERN).is(OR_PATTERN, "as", CAPTURE_PATTERN);
472+
b.rule(OR_PATTERN).is(CLOSED_PATTERN, b.zeroOrMore("|", CLOSED_PATTERN));
471473
b.rule(CAPTURE_PATTERN).is(NAME);
472474

473475
b.rule(SEQUENCE_PATTERN).is(b.firstOf(
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-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 org.sonar.plugins.python.api.tree.OrPattern;
25+
import org.sonar.plugins.python.api.tree.Pattern;
26+
import org.sonar.plugins.python.api.tree.Token;
27+
import org.sonar.plugins.python.api.tree.Tree;
28+
import org.sonar.plugins.python.api.tree.TreeVisitor;
29+
30+
public class OrPatternImpl extends PyTree implements OrPattern {
31+
32+
private final List<Pattern> patterns;
33+
private final List<Token> separators;
34+
35+
public OrPatternImpl(List<Pattern> patterns, List<Token> separators) {
36+
this.patterns = patterns;
37+
this.separators = separators;
38+
}
39+
40+
@Override
41+
public List<Pattern> patterns() {
42+
return patterns;
43+
}
44+
45+
@Override
46+
public List<Token> separators() {
47+
return separators;
48+
}
49+
50+
@Override
51+
public void accept(TreeVisitor visitor) {
52+
visitor.visitOrPattern(this);
53+
}
54+
55+
@Override
56+
public Kind getKind() {
57+
return Tree.Kind.OR_PATTERN;
58+
}
59+
60+
@Override
61+
List<Tree> computeChildren() {
62+
List<Tree> children = new ArrayList<>();
63+
int i = 0;
64+
for (Pattern element : patterns) {
65+
children.add(element);
66+
if (i < separators.size()) {
67+
children.add(separators.get(i));
68+
}
69+
i++;
70+
}
71+
return children;
72+
}
73+
}

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -827,20 +827,31 @@ public Guard guard(AstNode guardNode) {
827827
}
828828

829829
public static Pattern pattern(AstNode pattern) {
830-
if (pattern.is(PythonGrammar.CLOSED_PATTERN)) {
831-
return closedPattern(pattern);
830+
if (pattern.is(PythonGrammar.OR_PATTERN)) {
831+
return orPattern(pattern);
832832
}
833833
return asPattern(pattern);
834834
}
835835

836+
private static Pattern orPattern(AstNode pattern) {
837+
List<Token> separators = punctuators(pattern, PythonPunctuator.OR);
838+
if (separators.isEmpty()) {
839+
return closedPattern(pattern.getFirstChild(PythonGrammar.CLOSED_PATTERN));
840+
}
841+
List<Pattern> patterns = pattern.getChildren(PythonGrammar.CLOSED_PATTERN).stream()
842+
.map(PythonTreeMaker::closedPattern)
843+
.collect(Collectors.toList());
844+
return new OrPatternImpl(patterns, separators);
845+
}
846+
836847
private static AsPattern asPattern(AstNode asPattern) {
837-
Pattern pattern = closedPattern(asPattern.getFirstChild(PythonGrammar.CLOSED_PATTERN));
848+
Pattern pattern = orPattern(asPattern.getFirstChild(PythonGrammar.OR_PATTERN));
838849
Token asKeyword = toPyToken(asPattern.getFirstChild(PythonKeyword.AS).getToken());
839850
Name alias = name(asPattern.getFirstChild(PythonGrammar.CAPTURE_PATTERN).getFirstChild());
840851
return new AsPatternImpl(pattern, asKeyword, alias);
841852
}
842853

843-
private static Pattern closedPattern(AstNode closedPattern) {
854+
public static Pattern closedPattern(AstNode closedPattern) {
844855
AstNode astNode = closedPattern.getFirstChild();
845856
if (astNode.is(PythonGrammar.LITERAL_PATTERN)) {
846857
return literalPattern(astNode);

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,10 +412,21 @@ public void as_pattern() {
412412
verify(visitor).visitName(pattern.alias());
413413
}
414414

415+
@Test
416+
public void or_pattern() {
417+
setRootRule(PythonGrammar.OR_PATTERN);
418+
OrPattern pattern = ((OrPattern) parse("'foo' | 'bar'", PythonTreeMaker::pattern));
419+
FirstLastTokenVerifierVisitor visitor = spy(FirstLastTokenVerifierVisitor.class);
420+
pattern.accept(visitor);
421+
422+
verify(visitor).visitLiteralPattern((LiteralPattern) pattern.patterns().get(0));
423+
verify(visitor).visitLiteralPattern((LiteralPattern) pattern.patterns().get(1));
424+
}
425+
415426
@Test
416427
public void capture_pattern() {
417428
setRootRule(PythonGrammar.CLOSED_PATTERN);
418-
CapturePattern pattern = ((CapturePattern) parse("x", PythonTreeMaker::pattern));
429+
CapturePattern pattern = ((CapturePattern) parse("x", PythonTreeMaker::closedPattern));
419430
FirstLastTokenVerifierVisitor visitor = spy(FirstLastTokenVerifierVisitor.class);
420431
pattern.accept(visitor);
421432

@@ -425,7 +436,7 @@ public void capture_pattern() {
425436
@Test
426437
public void sequence_pattern() {
427438
setRootRule(PythonGrammar.CLOSED_PATTERN);
428-
SequencePattern pattern = ((SequencePattern) parse("[x, y]", PythonTreeMaker::pattern));
439+
SequencePattern pattern = ((SequencePattern) parse("[x, y]", PythonTreeMaker::closedPattern));
429440
FirstLastTokenVerifierVisitor visitor = spy(FirstLastTokenVerifierVisitor.class);
430441
pattern.accept(visitor);
431442

@@ -436,7 +447,7 @@ public void sequence_pattern() {
436447
@Test
437448
public void star_pattern() {
438449
setRootRule(PythonGrammar.CLOSED_PATTERN);
439-
SequencePattern pattern = ((SequencePattern) parse("[x, *rest]", PythonTreeMaker::pattern));
450+
SequencePattern pattern = ((SequencePattern) parse("[x, *rest]", PythonTreeMaker::closedPattern));
440451
FirstLastTokenVerifierVisitor visitor = spy(FirstLastTokenVerifierVisitor.class);
441452
pattern.accept(visitor);
442453

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
import org.sonar.plugins.python.api.tree.ListLiteral;
3030
import org.sonar.plugins.python.api.tree.LiteralPattern;
3131
import org.sonar.plugins.python.api.tree.MatchStatement;
32+
import org.sonar.plugins.python.api.tree.OrPattern;
3233
import org.sonar.plugins.python.api.tree.Pattern;
3334
import org.sonar.plugins.python.api.tree.SequencePattern;
3435
import org.sonar.plugins.python.api.tree.StarPattern;
36+
import org.sonar.plugins.python.api.tree.Token;
3537
import org.sonar.plugins.python.api.tree.Tree;
3638
import org.sonar.plugins.python.api.tree.Tree.Kind;
3739
import org.sonar.plugins.python.api.tree.Tuple;
@@ -153,6 +155,16 @@ public void literal_patterns() {
153155
assertThat(literalPattern.valueAsString()).isEqualTo("False");
154156
}
155157

158+
@Test
159+
public void or_pattern() {
160+
OrPattern pattern = pattern("case 42 | None | True: ...");
161+
assertThat(pattern.patterns()).extracting(p -> ((LiteralPattern) p).valueAsString()).containsExactly("42", "None", "True");
162+
assertThat(pattern.separators()).extracting(Token::value).containsExactly("|", "|");
163+
164+
AsPattern asPattern = pattern("case 'foo' | 'bar' as x: ...");
165+
assertThat(asPattern.pattern().getKind()).isEqualTo(Kind.OR_PATTERN);
166+
}
167+
156168
@Test
157169
public void as_pattern() {
158170
setRootRule(PythonGrammar.CASE_BLOCK);

0 commit comments

Comments
 (0)