Skip to content

Commit d6f06a4

Browse files
joke1196sonartech
authored andcommitted
SONARPY-917: Supporting GROUP PATTERN in match statements (#1099)
GitOrigin-RevId: 16520e9871862f13084ca25968708ef92cfdb1bc
1 parent 1298c19 commit d6f06a4

3 files changed

Lines changed: 144 additions & 1 deletion

File tree

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,45 @@ def match_irrefutable_in_middle(x):
152152
case 1:
153153
pass
154154

155+
def match_group_capture_is_irrefutable(x):
156+
match x:
157+
case 0:
158+
return x
159+
case (y):
160+
return y
161+
162+
163+
def match_group_wildcard_is_irrefutable(x):
164+
match x:
165+
case 0:
166+
return x
167+
case (_):
168+
return 42
169+
170+
171+
def match_nested_group_is_irrefutable(x):
172+
match x:
173+
case 0:
174+
return x
175+
case ((y)):
176+
return y
177+
178+
179+
def match_guarded_group_is_refutable(x): # Noncompliant
180+
match x:
181+
case 0:
182+
return x
183+
case (y) if x > 0:
184+
return y
185+
186+
187+
def match_group_literal_is_refutable(x): # Noncompliant
188+
match x:
189+
case 0:
190+
return x
191+
case (42):
192+
return 42
193+
155194
# raise / assert
156195

157196
def return_value_or_raise(x):

python-frontend/src/main/java/org/sonar/python/cfg/ControlFlowGraphBuilder.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.sonar.plugins.python.api.tree.IfStatement;
4343
import org.sonar.plugins.python.api.tree.MatchStatement;
4444
import org.sonar.plugins.python.api.tree.ParameterList;
45+
import org.sonar.plugins.python.api.tree.GroupPattern;
4546
import org.sonar.plugins.python.api.tree.Pattern;
4647
import org.sonar.plugins.python.api.tree.RaiseStatement;
4748
import org.sonar.plugins.python.api.tree.ReturnStatement;
@@ -251,7 +252,17 @@ private static int findIrrefutableCaseIndex(List<CaseBlock> caseBlocks) {
251252
}
252253

253254
private static boolean isIrrefutablePattern(CaseBlock caseBlock) {
254-
return caseBlock.guard() == null && caseBlock.pattern().is(Tree.Kind.WILDCARD_PATTERN, Tree.Kind.CAPTURE_PATTERN);
255+
return caseBlock.guard() == null && isIrrefutablePatternKind(caseBlock.pattern());
256+
}
257+
258+
private static boolean isIrrefutablePatternKind(Pattern pattern) {
259+
if (pattern.is(Tree.Kind.WILDCARD_PATTERN, Tree.Kind.CAPTURE_PATTERN)) {
260+
return true;
261+
}
262+
if (pattern.is(Tree.Kind.GROUP_PATTERN)) {
263+
return isIrrefutablePatternKind(((GroupPattern) pattern).pattern());
264+
}
265+
return false;
255266
}
256267

257268
private PythonCfgBlock buildWithStatement(WithStatement withStatement, PythonCfgBlock successor) {

python-frontend/src/test/java/org/sonar/plugins/python/api/cfg/ControlFlowGraphTest.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,99 @@ void match_statement_guarded_wildcard_is_refutable() {
752752
assertThat(guardedWildcard.falseSuccessor()).isEqualTo(barBlock);
753753
}
754754

755+
@Test
756+
void match_statement_group_pattern_is_irrefutable() {
757+
ControlFlowGraph cfg = cfg("""
758+
foo() """,
759+
"match x:",
760+
" case \"a\": y()",
761+
" case (z): w()",
762+
"bar()"
763+
);
764+
765+
PythonCfgBranchingBlock start = (PythonCfgBranchingBlock) cfg.start();
766+
PythonCfgEndBlock end = (PythonCfgEndBlock) cfg.end();
767+
assertThat(end.predecessors()).hasSize(1);
768+
CfgBlock barBlock = end.predecessors().stream().findFirst().get();
769+
770+
// case "a" false successor must be the irrefutable block (case (z)), not barBlock
771+
PythonCfgSimpleBlock irrefutableBlock = (PythonCfgSimpleBlock) start.falseSuccessor();
772+
assertThat(irrefutableBlock.elements()).extracting(Tree::getKind)
773+
.containsExactly(Kind.NAME, Kind.GROUP_PATTERN);
774+
775+
CfgBlock irrefutableBody = irrefutableBlock.successors().stream().findFirst().get();
776+
assertThat(irrefutableBody.successors()).containsExactly(barBlock);
777+
}
778+
779+
@Test
780+
void match_statement_group_wildcard_pattern_is_irrefutable() {
781+
ControlFlowGraph cfg = cfg("""
782+
foo() """,
783+
"match x:",
784+
" case \"a\": y()",
785+
" case (_): w()",
786+
"bar()"
787+
);
788+
789+
PythonCfgBranchingBlock start = (PythonCfgBranchingBlock) cfg.start();
790+
// case "a" false successor must be a simple block (irrefutable), not a branching block
791+
PythonCfgSimpleBlock irrefutableBlock = (PythonCfgSimpleBlock) start.falseSuccessor();
792+
assertThat(irrefutableBlock.elements()).extracting(Tree::getKind)
793+
.containsExactly(Kind.NAME, Kind.GROUP_PATTERN);
794+
}
795+
796+
@Test
797+
void match_statement_nested_group_pattern_is_irrefutable() {
798+
ControlFlowGraph cfg = cfg("""
799+
foo() """,
800+
"match x:",
801+
" case \"a\": y()",
802+
" case ((z)): w()",
803+
"bar()"
804+
);
805+
806+
PythonCfgBranchingBlock start = (PythonCfgBranchingBlock) cfg.start();
807+
// case "a" false successor must be a simple block (irrefutable), not a branching block
808+
PythonCfgSimpleBlock irrefutableBlock = (PythonCfgSimpleBlock) start.falseSuccessor();
809+
assertThat(irrefutableBlock.elements()).extracting(Tree::getKind)
810+
.containsExactly(Kind.NAME, Kind.GROUP_PATTERN);
811+
}
812+
813+
@Test
814+
void match_statement_group_literal_pattern_is_refutable() {
815+
ControlFlowGraph cfg = cfg("""
816+
foo() """,
817+
"match x:",
818+
" case \"a\": y()",
819+
" case (42): w()",
820+
"bar()"
821+
);
822+
823+
// Both cases are refutable — the second case must be a branching block
824+
PythonCfgBranchingBlock start = (PythonCfgBranchingBlock) cfg.start();
825+
PythonCfgBranchingBlock secondCase = (PythonCfgBranchingBlock) start.falseSuccessor();
826+
assertThat(secondCase).isInstanceOf(PythonCfgBranchingBlock.class);
827+
// The false path of the second case leads directly to barBlock (no-match path)
828+
PythonCfgEndBlock end = (PythonCfgEndBlock) cfg.end();
829+
CfgBlock barBlock = end.predecessors().stream().findFirst().get();
830+
assertThat(secondCase.falseSuccessor()).isEqualTo(barBlock);
831+
}
832+
833+
@Test
834+
void match_statement_guarded_group_pattern_is_refutable() {
835+
ControlFlowGraph cfg = cfg("""
836+
foo() """,
837+
"match x:",
838+
" case \"a\": y()",
839+
" case (z) if w(): v()",
840+
"bar()"
841+
);
842+
843+
// Both cases are refutable — the second case must be a branching block
844+
PythonCfgBranchingBlock start = (PythonCfgBranchingBlock) cfg.start();
845+
assertThat(start.falseSuccessor()).isInstanceOf(PythonCfgBranchingBlock.class);
846+
}
847+
755848
@Test
756849
void CFGBlock_toString() {
757850
PythonCfgEndBlock endBlock = new PythonCfgEndBlock();

0 commit comments

Comments
 (0)