Skip to content

Commit fb40b95

Browse files
committed
SONARPY-2018 Only analyze notebooks with a Python kernel (#1893)
1 parent 23dcdec commit fb40b95

File tree

5 files changed

+151
-6
lines changed

5 files changed

+151
-6
lines changed

sonar-python-plugin/src/main/java/org/sonar/plugins/python/IPynbSensor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ private static List<PythonInputFile> parseNotebooks(List<PythonInputFile> python
9898
List<PythonInputFile> generatedIPythonFiles = new ArrayList<>();
9999
for (PythonInputFile inputFile : pythonFiles) {
100100
try {
101-
PythonInputFile result = IpynbNotebookParser.parseNotebook(inputFile);
102-
generatedIPythonFiles.add(result);
101+
var result = IpynbNotebookParser.parseNotebook(inputFile);
102+
result.ifPresent(generatedIPythonFiles::add);
103103
} catch (Exception e) {
104104
if (context.config().getBoolean(FAIL_FAST_PROPERTY_NAME).orElse(false) && !isErrorOnTestFile(inputFile)) {
105105
throw new IllegalStateException("Exception when parsing " + inputFile, e);

sonar-python-plugin/src/main/java/org/sonar/plugins/python/IpynbNotebookParser.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,19 @@
2727
import java.util.HashMap;
2828
import java.util.LinkedHashMap;
2929
import java.util.Map;
30+
import java.util.Optional;
31+
import java.util.Set;
3032
import org.sonar.python.IPythonLocation;
3133

3234
public class IpynbNotebookParser {
3335

3436
public static final String SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER = "#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER";
3537

36-
public static GeneratedIPythonFile parseNotebook(PythonInputFile inputFile) {
38+
private static final Set<String> ACCEPTED_LANGUAGE = Set.of("python", "ipython");
39+
40+
public static Optional<GeneratedIPythonFile> parseNotebook(PythonInputFile inputFile) {
3741
try {
38-
return new IpynbNotebookParser(inputFile).parseNotebook();
42+
return new IpynbNotebookParser(inputFile).parse();
3943
} catch (IOException e) {
4044
throw new IllegalStateException("Cannot read " + inputFile, e);
4145
}
@@ -54,6 +58,31 @@ private IpynbNotebookParser(PythonInputFile inputFile) {
5458
private int lastPythonLine = 0;
5559
private boolean isFirstCell = true;
5660

61+
public Optional<GeneratedIPythonFile> parse() throws IOException {
62+
// If the language is not present, we assume it is a Python notebook
63+
var isPythonNotebook = parseLanguage().map(ACCEPTED_LANGUAGE::contains).orElse(true);
64+
65+
return Boolean.TRUE.equals(isPythonNotebook) ? Optional.of(parseNotebook()) : Optional.empty();
66+
}
67+
68+
public Optional<String> parseLanguage() throws IOException {
69+
String content = inputFile.wrappedFile().contents();
70+
JsonFactory factory = new JsonFactory();
71+
try (JsonParser jParser = factory.createParser(content)) {
72+
while (!jParser.isClosed()) {
73+
JsonToken jsonToken = jParser.nextToken();
74+
if (JsonToken.FIELD_NAME.equals(jsonToken)) {
75+
String fieldName = jParser.currentName();
76+
if ("language".equals(fieldName)) {
77+
jParser.nextToken();
78+
return Optional.ofNullable(jParser.getValueAsString());
79+
}
80+
}
81+
}
82+
}
83+
return Optional.empty();
84+
}
85+
5786
public GeneratedIPythonFile parseNotebook() throws IOException {
5887
String content = inputFile.wrappedFile().contents();
5988
JsonFactory factory = new JsonFactory();

sonar-python-plugin/src/test/java/org/sonar/plugins/python/IpynbNotebookParserTest.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ class IpynbNotebookParserTest {
3838
void testParseNotebook() throws IOException {
3939
var inputFile = createInputFile(baseDir, "notebook.ipynb", InputFile.Status.CHANGED, InputFile.Type.MAIN);
4040

41-
var result = IpynbNotebookParser.parseNotebook(inputFile);
41+
var resultOptional = IpynbNotebookParser.parseNotebook(inputFile);
42+
43+
assertThat(resultOptional).isPresent();
44+
45+
var result = resultOptional.get();
4246

4347
assertThat(result.locationMap().keySet()).hasSize(27);
4448
assertThat(result.contents()).hasLineCount(27);
@@ -60,7 +64,11 @@ void testParseNotebook() throws IOException {
6064
void testParseNotebookWithEmptyLines() throws IOException {
6165
var inputFile = createInputFile(baseDir, "notebook_with_empty_lines.ipynb", InputFile.Status.CHANGED, InputFile.Type.MAIN);
6266

63-
var result = IpynbNotebookParser.parseNotebook(inputFile);
67+
var resultOptional = IpynbNotebookParser.parseNotebook(inputFile);
68+
69+
assertThat(resultOptional).isPresent();
70+
71+
var result = resultOptional.get();
6472

6573
assertThat(result.locationMap().keySet()).hasSize(4);
6674
assertThat(result.contents()).hasLineCount(5);
@@ -81,4 +89,22 @@ void testParseInvalidNotebook() {
8189
.hasMessageContaining("Unexpected token");
8290
}
8391

92+
@Test
93+
void testParseMojoNotebook() {
94+
var inputFile = createInputFile(baseDir, "notebook_mojo.ipynb", InputFile.Status.CHANGED, InputFile.Type.MAIN);
95+
96+
var resultOptional = IpynbNotebookParser.parseNotebook(inputFile);
97+
98+
assertThat(resultOptional).isEmpty();
99+
}
100+
101+
@Test
102+
void testParseNotebookWithNoLanguage() {
103+
var inputFile = createInputFile(baseDir, "notebook_no_language.ipynb", InputFile.Status.CHANGED, InputFile.Type.MAIN);
104+
105+
var resultOptional = IpynbNotebookParser.parseNotebook(inputFile);
106+
107+
assertThat(resultOptional).isPresent();
108+
}
109+
84110
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 4,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"from math import sqrt\n",
10+
"from algorithm.functional import parallelize"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": 5,
16+
"metadata": {},
17+
"outputs": [],
18+
"source": [
19+
"struct Point:\n",
20+
" var x: Float32\n",
21+
" var y: Float32\n",
22+
"\n",
23+
" fn __init__(inout self, x: Float32, y: Float32):\n",
24+
" self.x = x\n",
25+
" self.y = y\n",
26+
"\n",
27+
" fn distance_from_origin(self):\n",
28+
" print(sqrt(self.x**2 + self.y**2))"
29+
]
30+
},
31+
{
32+
"cell_type": "code",
33+
"execution_count": 6,
34+
"metadata": {},
35+
"outputs": [
36+
{
37+
"name": "stdout",
38+
"output_type": "stream",
39+
"text": [
40+
"5.0\n"
41+
]
42+
}
43+
],
44+
"source": [
45+
"var p = Point(3.0, 4.0)\n",
46+
"p.distance_from_origin()"
47+
]
48+
}
49+
],
50+
"metadata": {
51+
"kernelspec": {
52+
"display_name": "Mojo",
53+
"language": "mojo",
54+
"name": "mojo-jupyter-kernel"
55+
},
56+
"language_info": {
57+
"codemirror_mode": {
58+
"name": "mojo"
59+
},
60+
"file_extension": ".mojo",
61+
"mimetype": "text/x-mojo",
62+
"name": "mojo"
63+
}
64+
},
65+
"nbformat": 4,
66+
"nbformat_minor": 2
67+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"cells": [],
3+
"metadata": {
4+
"kernelspec": {
5+
"display_name": "jupyter-experiment_venv",
6+
"name": "python3"
7+
},
8+
"language_info": {
9+
"codemirror_mode": {
10+
"name": "ipython",
11+
"version": 3
12+
},
13+
"file_extension": ".py",
14+
"mimetype": "text/x-python",
15+
"name": "python",
16+
"nbconvert_exporter": "python",
17+
"pygments_lexer": "ipython3",
18+
"version": "3.12.2"
19+
}
20+
},
21+
"nbformat": 4,
22+
"nbformat_minor": 2
23+
}

0 commit comments

Comments
 (0)