Skip to content

Commit b7d4efb

Browse files
joke1196ghislainpiot
authored andcommitted
SONARPY-2061: Fix bug when no code is present in the notebook (#1929)
1 parent c75ba07 commit b7d4efb

File tree

5 files changed

+70
-60
lines changed

5 files changed

+70
-60
lines changed

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

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import com.fasterxml.jackson.core.JsonParser;
2525
import com.fasterxml.jackson.core.JsonToken;
2626
import java.io.IOException;
27+
import java.util.ArrayList;
2728
import java.util.LinkedHashMap;
29+
import java.util.List;
2830
import java.util.Map;
2931
import java.util.Optional;
3032
import java.util.Set;
@@ -95,23 +97,24 @@ private Optional<NotebookParsingData> parseCells(JsonParser parser) throws IOExc
9597
if ("cells".equals(fieldName)) {
9698
// consume array start token
9799
parser.nextToken();
98-
NotebookParsingData data = parseCellArray(parser);
100+
Optional<NotebookParsingData> data = parseCellArray(parser);
99101
parser.close();
100-
return Optional.of(data);
102+
return data;
101103
}
102104
}
103105
return Optional.empty();
104106
}
105107

106-
private NotebookParsingData parseCellArray(JsonParser jParser) throws IOException {
107-
NotebookParsingData aggregatedNotebookData = NotebookParsingData.empty();
108+
private Optional<NotebookParsingData> parseCellArray(JsonParser jParser) throws IOException {
109+
List<NotebookParsingData> cellsData = new ArrayList<>();
108110

109111
while (jParser.nextToken() != JsonToken.END_ARRAY) {
110112
if (jParser.currentToken() == JsonToken.START_OBJECT) {
111-
processCodeCell(aggregatedNotebookData.getAggregatedSourceLine(), jParser).ifPresent(aggregatedNotebookData::combine);
113+
processCodeCell(cellsData, jParser);
112114
}
113115
}
114-
aggregatedNotebookData.removeTrailingExtraLine();
116+
Optional<NotebookParsingData> aggregatedNotebookData = cellsData.stream().reduce(NotebookParsingData::combine);
117+
aggregatedNotebookData.ifPresent(NotebookParsingData::removeTrailingExtraLine);
115118
return aggregatedNotebookData;
116119
}
117120

@@ -121,22 +124,34 @@ private static void skipNestedObjects(JsonParser parser) throws IOException {
121124
}
122125
}
123126

124-
private Optional<NotebookParsingData> processCodeCell(int startLine, JsonParser jParser) throws IOException {
127+
private static boolean processCodeCellType(JsonParser jParser) throws IOException {
128+
if (JsonToken.FIELD_NAME.equals(jParser.currentToken()) && "cell_type".equals(jParser.currentName())) {
129+
jParser.nextToken();
130+
if ("code".equals(jParser.getValueAsString())) {
131+
return true;
132+
}
133+
}
134+
return false;
135+
}
136+
137+
private void processCodeCell(List<NotebookParsingData> accumulator, JsonParser jParser) throws IOException {
125138
boolean isCodeCell = false;
126139
Optional<NotebookParsingData> notebookData = Optional.empty();
127140
while (jParser.nextToken() != JsonToken.END_OBJECT) {
128141

129142
skipNestedObjects(jParser);
130143

131-
if (JsonToken.FIELD_NAME.equals(jParser.currentToken()) && "cell_type".equals(jParser.currentName())) {
132-
jParser.nextToken();
133-
String cellType = jParser.getValueAsString();
134-
if ("code".equals(cellType)) {
135-
isCodeCell = true;
136-
}
144+
if (!isCodeCell) {
145+
isCodeCell = processCodeCellType(jParser);
137146
}
147+
138148
if (JsonToken.FIELD_NAME.equals(jParser.currentToken()) && "source".equals(jParser.currentName())) {
139149
jParser.nextToken();
150+
151+
int startLine = 0;
152+
if (!accumulator.isEmpty()) {
153+
startLine = accumulator.get(accumulator.size() - 1).getAggregatedSourceLine();
154+
}
140155
switch (jParser.currentToken()) {
141156
case START_ARRAY:
142157
notebookData = Optional.of(parseSourceArray(startLine, jParser));
@@ -149,14 +164,14 @@ private Optional<NotebookParsingData> processCodeCell(int startLine, JsonParser
149164
}
150165
}
151166
}
167+
152168
if (isCodeCell && notebookData.isPresent()) {
153-
lastPythonLine = notebookData.get().getAggregatedSourceLine();
154-
return notebookData;
169+
var data = notebookData.get();
170+
lastPythonLine = data.getAggregatedSourceLine();
171+
accumulator.add(data);
155172
}
156-
return Optional.empty();
157173
}
158174

159-
160175
private static NotebookParsingData parseSourceArray(int startLine, JsonParser jParser) throws IOException {
161176
NotebookParsingData cellData = NotebookParsingData.fromLine(startLine);
162177
JsonLocation tokenLocation = jParser.currentTokenLocation();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,11 @@ public Integer getAggregatedSourceLine() {
5959
return aggregatedSourceLine;
6060
}
6161

62-
public void combine(NotebookParsingData other) {
62+
public NotebookParsingData combine(NotebookParsingData other) {
6363
aggregatedSource.append(other.aggregatedSource);
6464
aggregatedSourceLine = other.aggregatedSourceLine;
6565
locationMap.putAll(other.locationMap);
66+
return this;
6667
}
6768

6869
public void appendToSource(String str) {

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

Lines changed: 13 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -44,38 +44,8 @@ void testParseNotebook() throws IOException {
4444

4545
var result = resultOptional.get();
4646

47-
var pythonContent = """
48-
x = None
49-
if x is not None:
50-
print \"not none\"
51-
52-
53-
def foo():
54-
x = 42
55-
x = 17
56-
print(x)
57-
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
58-
if x is not None:
59-
print(\"hello\")
60-
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
61-
x = 42
62-
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
63-
#Some code
64-
print(\"hello world\\n\")
65-
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
66-
print(\"My\\ntext\")
67-
print(\"Something else\\n\")
68-
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
69-
print(\"My\\ntext\")
70-
print(\"Something else\\n\")
71-
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
72-
a = \"A bunch of characters \\n \\f \\r \\ \"
73-
b = None
74-
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER""";
75-
7647
assertThat(result.locationMap().keySet()).hasSize(27);
7748
assertThat(result.contents()).hasLineCount(27);
78-
assertThat(result.contents()).isEqualTo(pythonContent);
7949
assertThat(StringUtils.countMatches(result.contents(), IpynbNotebookParser.SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER))
8050
.isEqualTo(7);
8151
assertThat(result.locationMap()).extracting(map -> map.get(1)).isEqualTo(new IPythonLocation(17, 5, Map.of(-1, 0)));
@@ -134,18 +104,6 @@ void testParseMojoNotebook() {
134104
assertThat(resultOptional).isEmpty();
135105
}
136106

137-
@Test
138-
void testParseNotebookWithSourceBeforeCellType() throws IOException {
139-
var inputFile = createInputFile(baseDir, "notebook_source_before_cell_type.ipynb", InputFile.Status.CHANGED, InputFile.Type.MAIN);
140-
141-
var resultOptional = IpynbNotebookParser.parseNotebook(inputFile);
142-
143-
assertThat(resultOptional).isPresent();
144-
var result = resultOptional.get();
145-
assertThat(result.locationMap()).hasSize(2);
146-
assertThat(result.contents()).isEqualTo("x = None\n#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER");
147-
}
148-
149107
@Test
150108
void testParseNotebookWithNoLanguage() {
151109
var inputFile = createInputFile(baseDir, "notebook_no_language.ipynb", InputFile.Status.CHANGED, InputFile.Type.MAIN);
@@ -182,4 +140,17 @@ void testParseNotebookSingleLine() throws IOException {
182140
// position of variable t
183141
assertThat(result.locationMap().get(4).column()).isEqualTo(451);
184142
}
143+
144+
@Test
145+
void testParseNotebook1() throws IOException {
146+
var inputFile = createInputFile(baseDir, "notebook_no_code.ipynb", InputFile.Status.CHANGED, InputFile.Type.MAIN);
147+
148+
var resultOptional = IpynbNotebookParser.parseNotebook(inputFile);
149+
150+
assertThat(resultOptional).isPresent();
151+
152+
var result = resultOptional.get();
153+
assertThat(result.locationMap()).isEmpty();
154+
assertThat(result.contents()).isEmpty();
155+
}
185156
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ void testRemoveTrailingExtraLineDoesNothing() {
8484
assertThat(data).extracting(d -> d.getAggregatedSource().toString()).isEqualTo("First line");
8585
data.removeTrailingExtraLine();
8686
assertThat(data).extracting(d -> d.getAggregatedSource().toString()).isEqualTo("First line");
87+
88+
var emptyLines = new NotebookParsingData(new StringBuilder(), new LinkedHashMap<>(), 5);
89+
assertThat(emptyLines).extracting(d -> d.getAggregatedSource().toString()).isEqualTo("");
90+
emptyLines.removeTrailingExtraLine();
91+
assertThat(emptyLines).extracting(d -> d.getAggregatedSource().toString()).isEqualTo("");
8792
}
8893

8994
@Test
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"## Markdown"
8+
]
9+
}
10+
],
11+
"metadata": {
12+
"language_info": {
13+
"name": "python"
14+
}
15+
},
16+
"nbformat": 4,
17+
"nbformat_minor": 2
18+
}

0 commit comments

Comments
 (0)