Skip to content

Commit 50b9bb9

Browse files
SONARPY-1299 Ensure rules unit tests can run on IPython files (#1408)
1 parent a3c4234 commit 50b9bb9

File tree

8 files changed

+157
-31
lines changed

8 files changed

+157
-31
lines changed

python-checks/src/test/java/org/sonar/python/checks/CaughtExceptionsCheckTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,37 @@ public void quickFixTest() {
5757
PythonQuickFixVerifier.verifyQuickFixMessages(check, before, expectedMessage);
5858
}
5959

60+
@Test
61+
public void quickFixIPythonTest() {
62+
var check = new CaughtExceptionsCheck();
63+
var before = "#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER\n" +
64+
"class CustomException:\n" +
65+
" ...\n" +
66+
"\n" +
67+
"#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER\n" +
68+
"def foo():\n" +
69+
" try:\n" +
70+
" a = %time bar()\n" +
71+
" except CustomException:\n" +
72+
" ...";
73+
74+
var after = "#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER\n" +
75+
"class CustomException(Exception):\n" +
76+
" ...\n" +
77+
"\n" +
78+
"#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER\n" +
79+
"def foo():\n" +
80+
" try:\n" +
81+
" a = %time bar()\n" +
82+
" except CustomException:\n" +
83+
" ...";
84+
85+
var expectedMessage = String.format(CaughtExceptionsCheck.QUICK_FIX_MESSAGE_FORMAT, "CustomException");
86+
87+
PythonQuickFixVerifier.verifyIPython(check, before, after);
88+
PythonQuickFixVerifier.verifyIPythonQuickFixMessages(check, before, expectedMessage);
89+
}
90+
6091
@Test
6192
public void exceptionWithEmptyParenthesisQuickFixTest() {
6293
var check = new CaughtExceptionsCheck();

python-checks/src/test/java/org/sonar/python/checks/DeadStoreCheckTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public void test() {
3535
PythonCheckVerifier.verify("src/test/resources/checks/deadStore.py", check);
3636
}
3737

38+
@Test
39+
public void iPythonTest() {
40+
PythonCheckVerifier.verify("src/test/resources/checks/deadStore.ipynb", check);
41+
}
42+
3843
@Test
3944
public void assignment_on_single_line() {
4045
String codeWithIssue = code(

python-checks/src/test/java/org/sonar/python/checks/quickfix/PythonQuickFixVerifier.java

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,27 @@
1919
*/
2020
package org.sonar.python.checks.quickfix;
2121

22-
import com.sonar.sslr.api.AstNode;
2322
import java.net.URI;
2423
import java.util.ArrayList;
2524
import java.util.Arrays;
2625
import java.util.Collections;
2726
import java.util.Comparator;
2827
import java.util.List;
28+
import java.util.function.Function;
2929
import java.util.stream.Collectors;
3030
import java.util.stream.Stream;
3131
import org.sonar.plugins.python.api.PythonCheck;
3232
import org.sonar.plugins.python.api.PythonCheck.PreciseIssue;
3333
import org.sonar.plugins.python.api.PythonFile;
3434
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
3535
import org.sonar.plugins.python.api.PythonVisitorContext;
36-
import org.sonar.plugins.python.api.tree.FileInput;
36+
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
37+
import org.sonar.plugins.python.api.quickfix.PythonTextEdit;
3738
import org.sonar.python.SubscriptionVisitor;
3839
import org.sonar.python.caching.CacheContextImpl;
3940
import org.sonar.python.parser.PythonParser;
40-
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
41-
import org.sonar.plugins.python.api.quickfix.PythonTextEdit;
4241
import org.sonar.python.semantic.ProjectLevelSymbolTable;
42+
import org.sonar.python.tree.IPythonTreeMaker;
4343
import org.sonar.python.tree.PythonTreeMaker;
4444

4545
import static org.assertj.core.api.Assertions.assertThat;
@@ -49,8 +49,32 @@ private PythonQuickFixVerifier() {
4949
}
5050

5151
public static void verify(PythonCheck check, String codeWithIssue, String... codesFixed) {
52+
verify(PythonQuickFixVerifier::createPythonVisitorContext, check, codeWithIssue, codesFixed);
53+
}
54+
55+
public static void verifyNoQuickFixes(PythonCheck check, String codeWithIssue) {
56+
verifyNoQuickFixes(PythonQuickFixVerifier::createPythonVisitorContext, check, codeWithIssue);
57+
}
58+
59+
public static void verifyQuickFixMessages(PythonCheck check, String codeWithIssue, String... expectedMessages) {
60+
verifyQuickFixMessages(PythonQuickFixVerifier::createPythonVisitorContext, check, codeWithIssue, expectedMessages);
61+
}
62+
63+
public static void verifyIPython(PythonCheck check, String codeWithIssue, String... codesFixed) {
64+
verify(PythonQuickFixVerifier::createIPythonVisitorContext, check, codeWithIssue, codesFixed);
65+
}
66+
67+
public static void verifyIPythonNoQuickFixes(PythonCheck check, String codeWithIssue) {
68+
verifyNoQuickFixes(PythonQuickFixVerifier::createIPythonVisitorContext, check, codeWithIssue);
69+
}
70+
71+
public static void verifyIPythonQuickFixMessages(PythonCheck check, String codeWithIssue, String... expectedMessages) {
72+
verifyQuickFixMessages(PythonQuickFixVerifier::createIPythonVisitorContext, check, codeWithIssue, expectedMessages);
73+
}
74+
75+
public static void verify(Function<String, PythonVisitorContext> createVisitorContext, PythonCheck check, String codeWithIssue, String... codesFixed) {
5276
List<PythonCheck.PreciseIssue> issues = PythonQuickFixVerifier
53-
.getIssuesWithQuickFix(check, codeWithIssue);
77+
.getIssuesWithQuickFix(createVisitorContext, check, codeWithIssue);
5478

5579
assertThat(issues)
5680
.as("Number of issues")
@@ -73,9 +97,9 @@ public static void verify(PythonCheck check, String codeWithIssue, String... cod
7397
.isEqualTo(Arrays.asList(codesFixed));
7498
}
7599

76-
public static void verifyNoQuickFixes(PythonCheck check, String codeWithIssue) {
100+
public static void verifyNoQuickFixes(Function<String, PythonVisitorContext> createVisitorContext, PythonCheck check, String codeWithIssue) {
77101
List<PythonCheck.PreciseIssue> issues = PythonQuickFixVerifier
78-
.getIssuesWithQuickFix(check, codeWithIssue);
102+
.getIssuesWithQuickFix(createVisitorContext, check, codeWithIssue);
79103

80104
assertThat(issues)
81105
.as("Number of issues")
@@ -89,9 +113,12 @@ public static void verifyNoQuickFixes(PythonCheck check, String codeWithIssue) {
89113
.isEmpty();
90114
}
91115

92-
public static void verifyQuickFixMessages(PythonCheck check, String codeWithIssue, String... expectedMessages) {
116+
public static void verifyQuickFixMessages(Function<String, PythonVisitorContext> createVisitorContext,
117+
PythonCheck check,
118+
String codeWithIssue,
119+
String... expectedMessages) {
93120
Stream<String> descriptions = PythonQuickFixVerifier
94-
.getIssuesWithQuickFix(check, codeWithIssue)
121+
.getIssuesWithQuickFix(createVisitorContext, check, codeWithIssue)
95122
.stream()
96123
.flatMap(issue -> issue.quickFixes().stream())
97124
.map(PythonQuickFix::getDescription);
@@ -107,17 +134,27 @@ private static List<PreciseIssue> scanFileForIssues(PythonCheck check, PythonVis
107134
return context.getIssues();
108135
}
109136

110-
private static List<PreciseIssue> getIssuesWithQuickFix(PythonCheck check, String codeWithIssue) {
111-
PythonParser parser = PythonParser.create();
112-
PythonQuickFixFile pythonFile = new PythonQuickFixFile(codeWithIssue);
113-
AstNode astNode = parser.parse(pythonFile.content());
114-
FileInput parse = new PythonTreeMaker().fileInput(astNode);
137+
private static List<PreciseIssue> getIssuesWithQuickFix(Function<String, PythonVisitorContext> createVisitorContext, PythonCheck check, String codeWithIssue) {
138+
var visitorContext = createVisitorContext.apply(codeWithIssue);
139+
return scanFileForIssues(check, visitorContext);
140+
}
141+
142+
private static PythonVisitorContext createPythonVisitorContext(String code) {
143+
return createVisitorContext(PythonParser.create(), new PythonTreeMaker(), code);
144+
}
145+
146+
private static PythonVisitorContext createIPythonVisitorContext(String code) {
147+
return createVisitorContext(PythonParser.createIPythonParser(), new IPythonTreeMaker(), code);
148+
}
115149

116-
PythonVisitorContext visitorContext = new PythonVisitorContext(parse,
150+
private static PythonVisitorContext createVisitorContext(PythonParser parser, PythonTreeMaker treeMaker, String code) {
151+
var pythonFile = new PythonQuickFixFile(code);
152+
var astNode = parser.parse(pythonFile.content());
153+
var fileInput = treeMaker.fileInput(astNode);
154+
155+
return new PythonVisitorContext(fileInput,
117156
pythonFile, null, "",
118157
ProjectLevelSymbolTable.empty(), CacheContextImpl.dummyCache());
119-
120-
return scanFileForIssues(check, visitorContext);
121158
}
122159

123160
private static String applyQuickFix(String codeWithIssue, PythonQuickFix quickFix) {

python-checks/src/test/java/org/sonar/python/checks/quickfix/PythonQuickFixVerifierTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ public void test_verify() {
7474
PythonQuickFixVerifier.verify(new SimpleCheck(), "a=10", "a!=10");
7575
}
7676

77+
@Test
78+
public void verifyIPython() {
79+
PythonQuickFixVerifier.verifyIPython(new SimpleCheck(), "a=10\n?a", "a!=10\n?a");
80+
PythonQuickFixVerifier.verifyIPythonQuickFixMessages(new SimpleCheck(), "a=10\n?a", "Add '!' here.");
81+
}
82+
7783
@Test
7884
public void test_multiple_lines() {
7985
PythonQuickFixVerifier.verify(new SimpleCheck(), "b \na=10", "b \na!=10");
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def fp_when_using_dynamic_object_information():
2+
x = 42 # Noncompliant
3+
x??
4+
x = 24
5+
print(x)

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

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

22-
import com.sonar.sslr.api.AstNode;
2322
import java.io.File;
2423
import java.io.IOException;
2524
import java.net.URI;
@@ -35,6 +34,7 @@
3534
import org.sonar.python.caching.CacheContextImpl;
3635
import org.sonar.python.parser.PythonParser;
3736
import org.sonar.python.semantic.ProjectLevelSymbolTable;
37+
import org.sonar.python.tree.IPythonTreeMaker;
3838
import org.sonar.python.tree.PythonTreeMaker;
3939

4040
import static org.sonar.python.semantic.SymbolUtils.pythonPackageName;
@@ -62,25 +62,33 @@ public static PythonVisitorContext createContext(File file, @Nullable File worki
6262

6363
public static PythonVisitorContext createContext(File file, @Nullable File workingDirectory, String packageName,
6464
ProjectLevelSymbolTable projectLevelSymbolTable, CacheContext cacheContext) {
65-
PythonParser parser = PythonParser.create();
6665
TestPythonFile pythonFile = new TestPythonFile(file);
67-
AstNode astNode = parser.parse(pythonFile.content());
68-
FileInput rootTree = new PythonTreeMaker().fileInput(astNode);
66+
FileInput rootTree = parseFile(pythonFile);
6967
return new PythonVisitorContext(rootTree, pythonFile, workingDirectory, packageName, projectLevelSymbolTable, cacheContext);
7068
}
7169

7270
public static ProjectLevelSymbolTable globalSymbols(List<File> files, File baseDir) {
7371
ProjectLevelSymbolTable projectLevelSymbolTable = new ProjectLevelSymbolTable();
7472
for (File file : files) {
75-
TestPythonFile pythonFile = new TestPythonFile(file);
76-
AstNode astNode = PythonParser.create().parse(pythonFile.content());
77-
FileInput astRoot = new PythonTreeMaker().fileInput(astNode);
73+
var pythonFile = new TestPythonFile(file);
74+
if (pythonFile.isIPython()) {
75+
continue;
76+
}
77+
var astRoot = parseFile(pythonFile);
7878
String packageName = pythonPackageName(file, baseDir.getAbsolutePath());
7979
projectLevelSymbolTable.addModule(astRoot, packageName, pythonFile);
8080
}
8181
return projectLevelSymbolTable;
8282
}
8383

84+
private static FileInput parseFile(TestPythonFile file) {
85+
var parser = file.isIPython() ? PythonParser.createIPythonParser() : PythonParser.create();
86+
var treeMaker = file.isIPython() ? new IPythonTreeMaker() : new PythonTreeMaker();
87+
88+
var astNode = parser.parse(file.content());
89+
return treeMaker.fileInput(astNode);
90+
}
91+
8492
private static class TestPythonFile implements PythonFile {
8593

8694
private final File file;
@@ -113,6 +121,10 @@ public String key() {
113121
return file.getPath();
114122
}
115123

124+
public boolean isIPython() {
125+
return fileName().endsWith(".ipynb");
126+
}
127+
116128
}
117129

118130
}

python-frontend/src/test/java/org/sonar/python/TestPythonVisitorRunnerTest.java

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import java.io.File;
2323
import java.io.IOException;
2424
import java.nio.file.Files;
25-
import java.util.Collections;
25+
import java.util.List;
2626
import org.junit.Test;
2727
import org.sonar.plugins.python.api.PythonVisitorContext;
2828
import org.sonar.plugins.python.api.symbols.Symbol;
@@ -33,21 +33,41 @@
3333
public class TestPythonVisitorRunnerTest {
3434

3535
@Test(expected = IllegalStateException.class)
36-
public void unknown_file() {
36+
public void unknownFile() {
3737
TestPythonVisitorRunner.scanFile(new File("xxx"), visitorContext -> {});
3838
}
3939

4040
@Test
41-
public void file_uri() throws IOException {
42-
File tmpFile = Files.createTempFile("foo", "py").toFile();
41+
public void fileUri() throws IOException {
42+
File tmpFile = Files.createTempFile("foo", ".py").toFile();
4343
PythonVisitorContext context = TestPythonVisitorRunner.createContext(tmpFile);
4444
assertThat(context.pythonFile().uri()).isEqualTo(tmpFile.toURI());
4545
}
4646

4747
@Test
48-
public void global_symbols() {
48+
public void fileUriIPython() throws IOException {
49+
File tmpFile = Files.createTempFile("foo", ".ipynb").toFile();
50+
PythonVisitorContext context = TestPythonVisitorRunner.createContext(tmpFile);
51+
assertThat(context.pythonFile().uri()).isEqualTo(tmpFile.toURI());
52+
}
53+
54+
@Test
55+
public void globalSymbols() {
56+
File baseDir = new File("src/test/resources").getAbsoluteFile();
57+
List<File> files = List.of(new File(baseDir, "file.py"));
58+
ProjectLevelSymbolTable projectLevelSymbolTable = TestPythonVisitorRunner.globalSymbols(files, baseDir);
59+
assertThat(projectLevelSymbolTable.getSymbolsFromModule("file"))
60+
.extracting(Symbol::name)
61+
.containsExactlyInAnyOrder("hello", "A");
62+
}
63+
64+
@Test
65+
public void globalSymbolsIPython() {
4966
File baseDir = new File("src/test/resources").getAbsoluteFile();
50-
ProjectLevelSymbolTable projectLevelSymbolTable = TestPythonVisitorRunner.globalSymbols(Collections.singletonList(new File(baseDir, "file.py")), baseDir);
51-
assertThat(projectLevelSymbolTable.getSymbolsFromModule("file")).extracting(Symbol::name).containsExactlyInAnyOrder("hello", "A");
67+
List<File> files = List.of(new File(baseDir, "file.py"), new File(baseDir, "file.ipynb"));
68+
ProjectLevelSymbolTable projectLevelSymbolTable = TestPythonVisitorRunner.globalSymbols(files, baseDir);
69+
assertThat(projectLevelSymbolTable.getSymbolsFromModule("file"))
70+
.extracting(Symbol::name)
71+
.containsExactlyInAnyOrder("hello", "A");
5272
}
5373
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
2+
def hello2():
3+
print("Hello world")
4+
return [
5+
]
6+
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
7+
#A comment
8+
class B:
9+
def method(self):
10+
pass

0 commit comments

Comments
 (0)