Skip to content

Commit d12b017

Browse files
SONARPY-923 Analyze regex in variables whose values we can infer (#978)
* Check regex pattern in variables * Make regex cache depend on flagSet * Add unit test for regex cache in frontend package * Remove regex cache tests from checks package * Small test refactoring
1 parent 2f05ae1 commit d12b017

File tree

5 files changed

+80
-23
lines changed

5 files changed

+80
-23
lines changed

python-checks/src/main/java/org/sonar/python/checks/regex/AbstractRegexCheck.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
import org.sonar.plugins.python.api.tree.BinaryExpression;
3636
import org.sonar.plugins.python.api.tree.CallExpression;
3737
import org.sonar.plugins.python.api.tree.Expression;
38+
import org.sonar.plugins.python.api.tree.Name;
3839
import org.sonar.plugins.python.api.tree.QualifiedExpression;
3940
import org.sonar.plugins.python.api.tree.RegularArgument;
4041
import org.sonar.plugins.python.api.tree.StringElement;
4142
import org.sonar.plugins.python.api.tree.StringLiteral;
4243
import org.sonar.plugins.python.api.tree.Tree;
44+
import org.sonar.python.checks.Expressions;
4345
import org.sonar.python.regex.PythonRegexIssueLocation;
4446
import org.sonar.python.regex.RegexContext;
4547
import org.sonar.python.tree.TreeUtils;
@@ -129,9 +131,13 @@ private static Optional<StringLiteral> patternArgStringLiteral(CallExpression re
129131
if (patternArgument == null) {
130132
return Optional.empty();
131133
}
132-
Expression patternArgumentExpression = patternArgument.expression();
133-
if (patternArgumentExpression.is(Tree.Kind.STRING_LITERAL)) {
134-
return Optional.of((StringLiteral) patternArgumentExpression);
134+
Expression patternValueExpression = patternArgument.expression();
135+
if (patternValueExpression.is(Tree.Kind.NAME)) {
136+
patternValueExpression = Expressions.singleAssignedValue((Name) patternValueExpression);
137+
}
138+
139+
if (patternValueExpression != null && patternValueExpression.is(Tree.Kind.STRING_LITERAL)) {
140+
return Optional.of((StringLiteral) patternValueExpression);
135141
}
136142
return Optional.empty();
137143
}

python-checks/src/test/java/org/sonar/python/checks/regex/AbstractRegexCheckTest.java

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import java.io.File;
2323
import java.util.ArrayList;
24-
import java.util.Arrays;
2524
import java.util.Collections;
2625
import java.util.List;
2726
import java.util.regex.Pattern;
@@ -58,20 +57,8 @@ public void checkRegex(RegexParseResult regexParseResult, CallExpression regexFu
5857

5958
PythonVisitorContext fileContext = TestPythonVisitorRunner.createContext(FILE);
6059
SubscriptionVisitor.analyze(Collections.singletonList(check), fileContext);
61-
assertThat(check.reportedRegexTrees).hasSize(11);
62-
assertThat(fileContext.getIssues()).hasSize(11);
63-
}
64-
65-
@Test
66-
public void test_regex_parse_result_is_retrieved_from_cache_in_context() {
67-
PythonVisitorContext fileContext = TestPythonVisitorRunner.createContext(FILE);
68-
Check checkOne = new Check();
69-
Check checkTwo = new Check();
70-
SubscriptionVisitor.analyze(Arrays.asList(checkOne, checkTwo), fileContext);
71-
assertThat(checkOne.receivedRegexParseResults).hasSameSizeAs(checkTwo.receivedRegexParseResults);
72-
for (int i = 0; i < checkOne.receivedRegexParseResults.size(); i++) {
73-
assertThat(checkOne.receivedRegexParseResults.get(i)).isSameAs(checkTwo.receivedRegexParseResults.get(i));
74-
}
60+
assertThat(check.reportedRegexTrees).hasSize(12);
61+
assertThat(fileContext.getIssues()).hasSize(12);
7562
}
7663

7764
@Test
@@ -81,7 +68,6 @@ public void test_flags() {
8168
}
8269

8370
private static class Check extends AbstractRegexCheck {
84-
private final List<RegexParseResult> receivedRegexParseResults = new ArrayList<>();
8571
private boolean printFlags;
8672

8773
private Check() {
@@ -94,7 +80,6 @@ private Check(boolean printFlags) {
9480

9581
@Override
9682
public void checkRegex(RegexParseResult regexParseResult, CallExpression regexFunctionCall) {
97-
receivedRegexParseResults.add(regexParseResult);
9883
addIssue(regexParseResult.getResult(), printFlags ? flagsToString(regexParseResult) : "MESSAGE", null, Collections.emptyList());
9984
}
10085

python-checks/src/test/resources/checks/regex/abstractRegexCheck.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
re.findall(r'.*', "foo") # Noncompliant
1212
re.finditer('.*', "foo") # Noncompliant
1313

14+
some_pattern = '.*' # Noncompliant
15+
re.match(some_pattern, 'some string')
16+
re.match(some_unknown_pattern, 'some string')
17+
re.match(get_pattern(), 'some string')
18+
1419
re.match('.*\N{GREEK SMALL LETTER FINAL SIGMA}', 'foo') # We do ignore not raw strings containing \N escape sequences
1520
re.match(r'.*\N{GREEK SMALL LETTER FINAL SIGMA}', 'foo') # Noncompliant
1621

python-frontend/src/main/java/org/sonar/python/SubscriptionVisitor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public class SubscriptionVisitor {
5858
private final EnumMap<Kind, List<SubscriptionContextImpl>> consumers = new EnumMap<>(Kind.class);
5959
private final PythonVisitorContext pythonVisitorContext;
6060
private Tree currentElement;
61-
private final HashMap<StringElement, RegexParseResult> regexCache = new HashMap<>();
61+
private final HashMap<String, RegexParseResult> regexCache = new HashMap<>();
6262

6363
public static void analyze(Collection<PythonSubscriptionCheck> checks, PythonVisitorContext pythonVisitorContext) {
6464
SubscriptionVisitor subscriptionVisitor = new SubscriptionVisitor(checks, pythonVisitorContext);
@@ -169,8 +169,8 @@ public File workingDirectory() {
169169
}
170170

171171
public RegexParseResult regexForStringElement(StringElement stringElement, FlagSet flagSet) {
172-
return regexCache.computeIfAbsent(stringElement,
173-
s -> new RegexParser(new PythonAnalyzerRegexSource(s), flagSet).parse());
172+
return regexCache.computeIfAbsent(stringElement.hashCode() + "-" + flagSet.getMask(),
173+
s -> new RegexParser(new PythonAnalyzerRegexSource(stringElement), flagSet).parse());
174174
}
175175
}
176176
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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;
21+
22+
import java.util.Collections;
23+
import java.util.regex.Pattern;
24+
import org.junit.Test;
25+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
26+
import org.sonar.plugins.python.api.PythonVisitorContext;
27+
import org.sonar.plugins.python.api.tree.FileInput;
28+
import org.sonar.plugins.python.api.tree.StringElement;
29+
import org.sonar.plugins.python.api.tree.Tree;
30+
import org.sonar.python.regex.RegexContext;
31+
import org.sonarsource.analyzer.commons.regex.RegexParseResult;
32+
import org.sonarsource.analyzer.commons.regex.ast.FlagSet;
33+
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
36+
public class SubscriptionVisitorTest {
37+
38+
@Test
39+
public void test_regex_cache() {
40+
PythonSubscriptionCheck check = new PythonSubscriptionCheck() {
41+
@Override
42+
public void initialize(Context context) {
43+
context.registerSyntaxNodeConsumer(Tree.Kind.STRING_ELEMENT, ctx -> {
44+
StringElement stringElement = (StringElement)ctx.syntaxNode();
45+
RegexContext regexCtx = (RegexContext) ctx;
46+
RegexParseResult resultWithNoFlags = regexCtx.regexForStringElement(stringElement, new FlagSet());
47+
RegexParseResult resultWithFlags = regexCtx.regexForStringElement(stringElement, new FlagSet(Pattern.MULTILINE));
48+
49+
assertThat(resultWithNoFlags).isNotSameAs(resultWithFlags);
50+
// When we retrieve them again, it will be the same instance retrieved from the cache.
51+
assertThat(resultWithNoFlags).isSameAs(regexCtx.regexForStringElement(stringElement, new FlagSet()));
52+
assertThat(resultWithFlags).isSameAs(regexCtx.regexForStringElement(stringElement, new FlagSet(Pattern.MULTILINE)));
53+
});
54+
}
55+
};
56+
57+
FileInput fileInput = PythonTestUtils.parse("'.*'");
58+
PythonVisitorContext context = new PythonVisitorContext(fileInput, PythonTestUtils.pythonFile("file"), null, null);
59+
SubscriptionVisitor.analyze(Collections.singleton(check), context);
60+
}
61+
}

0 commit comments

Comments
 (0)