Skip to content

Commit 039867a

Browse files
committed
SONARPY-2032 Adapt (executable) lines of code metric for Jupyter Notebooks (#1883)
1 parent 520b71a commit 039867a

File tree

6 files changed

+68
-5
lines changed

6 files changed

+68
-5
lines changed

python-frontend/src/main/java/org/sonar/python/metrics/FileLinesVisitor.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,18 @@ public class FileLinesVisitor extends PythonSubscriptionCheck {
6161
private final Set<Integer> linesOfComments = new HashSet<>();
6262
private final Set<Integer> linesOfDocstring = new HashSet<>();
6363
private final Set<Integer> executableLines = new HashSet<>();
64+
private final boolean isNotebook;
6465
private int statements = 0;
6566
private int classDefs = 0;
6667

68+
public FileLinesVisitor(boolean isNotebook) {
69+
this.isNotebook = isNotebook;
70+
}
71+
72+
public FileLinesVisitor() {
73+
this(false);
74+
}
75+
6776
@Override
6877
public void scanFile(PythonVisitorContext visitorContext) {
6978
SubscriptionVisitor.analyze(Collections.singleton(this), visitorContext);
@@ -158,7 +167,7 @@ private void visitComment(Trivia trivia, Token parentToken) {
158167
}
159168
}
160169

161-
public static boolean containsNoSonarComment(Trivia trivia){
170+
public static boolean containsNoSonarComment(Trivia trivia) {
162171
String commentLine = getContents(trivia.token().value());
163172
return commentLine.contains("NOSONAR");
164173
}
@@ -186,7 +195,7 @@ public int getCommentLineCount() {
186195
}
187196

188197
public Set<Integer> getExecutableLines() {
189-
return Collections.unmodifiableSet(executableLines);
198+
return isNotebook ? Set.of() : Collections.unmodifiableSet(executableLines);
190199
}
191200

192201
private static boolean isBlank(String line) {

python-frontend/src/main/java/org/sonar/python/metrics/FileMetrics.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ public class FileMetrics {
3535
private final FileLinesVisitor fileLinesVisitor;
3636
private List<Integer> functionComplexities = new ArrayList<>();
3737

38-
public FileMetrics(PythonVisitorContext context) {
38+
public FileMetrics(PythonVisitorContext context, boolean isNotebook) {
3939
FileInput fileInput = context.rootTree();
40-
fileLinesVisitor = new FileLinesVisitor();
40+
fileLinesVisitor = new FileLinesVisitor(isNotebook);
4141
fileLinesVisitor.scanFile(context);
4242
numberOfStatements = fileLinesVisitor.getStatements();
4343
numberOfClasses = fileLinesVisitor.getClassDefs();
@@ -46,6 +46,10 @@ public FileMetrics(PythonVisitorContext context) {
4646
fileInput.accept(new FunctionVisitor());
4747
}
4848

49+
public FileMetrics(PythonVisitorContext context) {
50+
this(context, false);
51+
}
52+
4953
private class FunctionVisitor extends BaseTreeVisitor {
5054
@Override
5155
public void visitFunctionDef(FunctionDef pyFunctionDefTree) {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,12 @@ void empty_file() {
6666
TestPythonVisitorRunner.scanFile(new File(BASE_DIR, "empty.py"), visitor);
6767
assertThat(visitor.getExecutableLines()).isEmpty();
6868
}
69+
70+
@Test
71+
void notebook_locs() {
72+
FileLinesVisitor visitor = new FileLinesVisitor(true);
73+
TestPythonVisitorRunner.scanFile(new File(BASE_DIR, "notebook_loc.ipynb"), visitor);
74+
assertThat(visitor.getExecutableLines()).isEmpty();
75+
assertThat(visitor.getLinesOfCode()).hasSize(17);
76+
}
6977
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
x = None
2+
if x is not None:
3+
print "not none"
4+
5+
6+
def foo():
7+
x = 42
8+
x = 17
9+
print(x)
10+
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
11+
if x is not None:
12+
print("hello")
13+
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
14+
x = 42
15+
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
16+
#Some code
17+
print("hello world\n")
18+
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
19+
print("My\ntext")
20+
print("Something else\n")
21+
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
22+
print("My\ntext")
23+
print("Something else\n")
24+
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER
25+
a = "A bunch of characters \n \t \f \r / // \ "
26+
b = None
27+
#SONAR_PYTHON_NOTEBOOK_CELL_DELIMITER

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ private static NewIssueLocation newLocation(PythonInputFile inputFile, NewIssue
302302
}
303303

304304
private void saveMeasures(PythonInputFile inputFile, PythonVisitorContext visitorContext) {
305-
FileMetrics fileMetrics = new FileMetrics(visitorContext);
305+
FileMetrics fileMetrics = new FileMetrics(visitorContext, isNotebook(inputFile));
306306
FileLinesVisitor fileLinesVisitor = fileMetrics.fileLinesVisitor();
307307

308308
noSonarFilter.noSonarInFile(inputFile.wrappedFile(), fileLinesVisitor.getLinesWithNoSonar());
@@ -330,6 +330,10 @@ private void saveMeasures(PythonInputFile inputFile, PythonVisitorContext visito
330330
}
331331
}
332332

333+
static boolean isNotebook(PythonInputFile inputFile) {
334+
return inputFile.kind() == PythonInputFile.Kind.IPYTHON;
335+
}
336+
333337
private boolean restoreAndPushMeasuresIfApplicable(PythonInputFile inputFile) {
334338
if (inputFile.wrappedFile().type() == InputFile.Type.TEST) {
335339
return true;

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,17 @@ void cpd_tokens_failure_does_not_execute_checks_multiple_times() throws IOExcept
12901290
.containsEntry(CPD_TOKENS_STRING_TABLE_KEY_PREFIX + inputFile.wrappedFile().key(), cpdTokens.stringTable);
12911291
}
12921292

1293+
@Test
1294+
void test_scanner_isNotebook(){
1295+
var regularPythonFile = mock(PythonInputFile.class);
1296+
when(regularPythonFile.kind()).thenReturn(PythonInputFile.Kind.PYTHON);
1297+
assertThat(PythonScanner.isNotebook(regularPythonFile)).isFalse();
1298+
1299+
var notebookPythonFile = mock(PythonInputFile.class);
1300+
when(notebookPythonFile.kind()).thenReturn(PythonInputFile.Kind.IPYTHON);
1301+
assertThat(PythonScanner.isNotebook(notebookPythonFile)).isTrue();
1302+
}
1303+
12931304
private com.sonar.sslr.api.Token passToken(URI uri) {
12941305
return com.sonar.sslr.api.Token.builder()
12951306
.setType(PythonKeyword.PASS)

0 commit comments

Comments
 (0)