Skip to content

Commit e935629

Browse files
SONARPY-977 Rule S6323: Alternation in regular expressions should not… (#1104)
1 parent 1fb0357 commit e935629

File tree

9 files changed

+149
-0
lines changed

9 files changed

+149
-0
lines changed

its/ruling/src/test/java/org/sonar/python/it/RulingHelper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ static List<String> bugRuleKeys() {
9090
"S5868",
9191
"S5996",
9292
"S6002",
93+
"S6323",
9394
"S905",
9495
"S930"
9596
);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
'nltk:nltk/corpus/reader/api.py':[
3+
75,
4+
],
5+
'nltk:nltk/data.py':[
6+
516,
7+
],
8+
}

python-checks/src/main/java/org/sonar/python/checks/CheckList.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.sonar.python.checks.regex.AnchorPrecedenceCheck;
5353
import org.sonar.python.checks.regex.DuplicatesInCharacterClassCheck;
5454
import org.sonar.python.checks.regex.EmptyGroupCheck;
55+
import org.sonar.python.checks.regex.EmptyAlternativeCheck;
5556
import org.sonar.python.checks.regex.EmptyStringRepetitionCheck;
5657
import org.sonar.python.checks.regex.GraphemeClustersInClassesCheck;
5758
import org.sonar.python.checks.regex.ImpossibleBoundariesCheck;
@@ -121,6 +122,7 @@ public static Iterable<Class> getChecks() {
121122
DynamicCodeExecutionCheck.class,
122123
ElseAfterLoopsWithoutBreakCheck.class,
123124
EmailSendingCheck.class,
125+
EmptyAlternativeCheck.class,
124126
EmptyGroupCheck.class,
125127
EmptyFunctionCheck.class,
126128
EmptyNestedBlockCheck.class,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 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.checks.regex;
21+
22+
import org.sonar.check.Rule;
23+
import org.sonar.plugins.python.api.tree.CallExpression;
24+
import org.sonarsource.analyzer.commons.regex.RegexParseResult;
25+
import org.sonarsource.analyzer.commons.regex.finders.EmptyAlternativeFinder;
26+
27+
@Rule(key = "S6323")
28+
public class EmptyAlternativeCheck extends AbstractRegexCheck{
29+
30+
@Override
31+
public void checkRegex(RegexParseResult regexParseResult, CallExpression regexFunctionCall) {
32+
new EmptyAlternativeFinder(this::addIssue).visit(regexParseResult);
33+
}
34+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<p>Alternation is used to match a single regular expression out of several possible regular expressions. If one of the alternatives is empty it would
2+
match any input, which is most probably a mistake.</p>
3+
<h2>Noncompliant Code Example</h2>
4+
<pre>
5+
re.search(r"Jack|Peter|", "John") # Noncompliant - will match an empty string
6+
re.search(r"Jack||Peter", "John") # Noncompliant - will match an empty string
7+
</pre>
8+
<h2>Compliant Solution</h2>
9+
<pre>
10+
re.search(r"Jack|Peter", "John") # returns false
11+
</pre>
12+
<h2>Exceptions</h2>
13+
<p>One could use an empty alternation to make a regular expression group optional. Rule will not report on such cases.</p>
14+
<pre>
15+
re.search(r"mandatory(-optional|)", "mandatory")
16+
re.search(r"mandatory(-optional|)", "mandatory-optional")
17+
</pre>
18+
<p>However, if there is a quantifier after the group the issue will be reported as using both <code>|</code> and quantifier is redundant.</p>
19+
<pre>
20+
re.search(r"mandatory(-optional|)?", "mandatory-optional") # Noncompliant - using both `|` inside the group and `?` for the group.
21+
</pre>
22+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"title": "Alternation in regular expressions should not contain empty alternatives",
3+
"type": "BUG",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"regex"
11+
],
12+
"defaultSeverity": "Major",
13+
"ruleSpecification": "RSPEC-6323",
14+
"sqKey": "S6323",
15+
"scope": "Main",
16+
"quickfix": "unknown"
17+
}

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
"S6002",
153153
"S6019",
154154
"S6035",
155+
"S6323",
155156
"S6331"
156157
]
157158
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 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.checks.regex;
21+
22+
import org.junit.Test;
23+
import org.sonar.python.checks.utils.PythonCheckVerifier;
24+
25+
public class EmptyAlternativeCheckTest {
26+
27+
@Test
28+
public void test() {
29+
PythonCheckVerifier.verify("src/test/resources/checks/regex/emptyAlternativeCheck.py", new EmptyAlternativeCheck());
30+
}
31+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import re
2+
3+
4+
def non_compliant(input):
5+
re.match("(mandatory||optional)", input) # Noncompliant {{Remove this empty alternative.}}
6+
# ^
7+
re.match("|mandatory|-optional", input) # Noncompliant
8+
# ^
9+
10+
re.match("mandatory|-optional|", input) # Noncompliant
11+
# ^
12+
# Noncompliant@+3
13+
re.match('''(
14+
\'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string
15+
| # or
16+
\S # - a non-whitespace character
17+
)''', input, re.X)
18+
19+
re.match("|mandatory|-optional", input) # Noncompliant
20+
re.match("(mandatory|(|O|o|)ptional|)", input) # Noncompliant
21+
re.match("(|mandatory|optional)?", input) # Noncompliant {{Remove this empty alternative.}}
22+
# ^
23+
re.match("mandatory(-optional|){2}", input) # Noncompliant
24+
# ^
25+
26+
27+
def compliant(input):
28+
re.match("(mandatory|optional|)", input)
29+
re.match("mandatory(-optional|)", input)
30+
re.match("mandatory(|-optional)", input)
31+
re.match("mandatory(|-optional)", input)
32+
re.match("mandatory(-optional|)", input)
33+
re.match("(mandatory(|-optional))?", input)

0 commit comments

Comments
 (0)