Skip to content

Commit 36d5687

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-3177 Implement S1309 for noqa (#390)
GitOrigin-RevId: 1c63c394268b3a586c428e2578289b6bdd6fb3a6
1 parent 7667f77 commit 36d5687

13 files changed

Lines changed: 230 additions & 24 deletions

File tree

its/plugin/it-python-plugin-test/profiles/nosonar.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,10 @@
1717
<key>OneStatementPerLine</key>
1818
<priority>INFO</priority>
1919
</rule>
20+
<rule>
21+
<repositoryKey>python</repositoryKey>
22+
<key>S1309</key>
23+
<priority>INFO</priority>
24+
</rule>
2025
</rules>
2126
</profile>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
print "Hello"
2+
print "Hello" # noqa
3+
print "Hello" # noqa: PrintStatementUsage
4+
print "Hello" # noqa: PrintStatementUsage, S1309
5+
6+
# noqa with comments
7+
print "Hello" # noqa This is a comment
8+
print "Hello" # noqa: PrintStatementUsage This is a comment
9+
10+
# multiple issues
11+
print "Hello"; print "World"
12+
print "Hello"; print "World" # noqa
13+
print "Hello"; print "World" # noqa: OneStatementPerLine,PrintStatementUsage
14+
print "Hello"; print "World" # noqa: OneStatementPerLine, PrintStatementUsage
15+
16+
# invalid noqa formats
17+
print "Hello" # noqa:
18+
print "Hello" # noqa: Invalid_Rule_Name
19+
print "Hello" # noqa PrintStatementUsage (missing colon)
20+
21+
# noqa at the end of file
22+
print "Hello" # noqa: PrintStatementUsage

its/plugin/it-python-plugin-test/src/test/java/com/sonar/python/it/plugin/NoSonarTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class NoSonarTest {
3030

3131
private static final String NO_SONAR_PROJECT_KEY = "nosonar";
3232
private static final String EXTERNAL_ISSUE_PROJECT_KEY = "external-issues";
33+
private static final String NOQA_PROJECT_KEY = "noqa";
3334

3435
private static final String PROFILE_NAME = "nosonar";
3536

@@ -95,6 +96,42 @@ void test_nosonar() {
9596
.doesNotContainIssue(20, "python:OneStatementPerLine");
9697
}
9798

99+
@Test
100+
void test_noqa() {
101+
analyzeProject(createScanner(NOQA_PROJECT_KEY, "projects/nosonar/noqa-project"));
102+
103+
IssueListAssert.assertThat(issues(NOQA_PROJECT_KEY))
104+
.hasSize(20)
105+
// basic noqa checks
106+
.containsIssue(1, "python:PrintStatementUsage")
107+
.containsIssue(2, "python:S1309").doesNotContainIssue(2, "python:PrintStatementUsage")
108+
.containsIssue(3, "python:S1309").doesNotContainIssue(3, "python:PrintStatementUsage")
109+
.containsIssue(4, "python:S1309").doesNotContainIssue(4, "python:PrintStatementUsage")
110+
111+
// noqa with specific rules
112+
.containsIssue(6, "python:S1309")
113+
.containsIssue(7, "python:S1309").doesNotContainIssue(6, "python:PrintStatementUsage")
114+
.containsIssue(8, "python:S1309").doesNotContainIssue(7, "python:PrintStatementUsage")
115+
116+
// noqa with multiple rules
117+
.containsIssue(11, "python:OneStatementPerLine").containsIssue(11, "python:PrintStatementUsage")
118+
.containsIssue(12, "python:S1309").doesNotContainIssue(12, "python:PrintStatementUsage")
119+
.doesNotContainIssue(12, "python:OneStatementPerLine")
120+
.containsIssue(13, "python:S1309").doesNotContainIssue(13, "python:PrintStatementUsage")
121+
.doesNotContainIssue(13, "python:OneStatementPerLine")
122+
.containsIssue(14, "python:S1309").doesNotContainIssue(14, "python:PrintStatementUsage")
123+
.doesNotContainIssue(14, "python:OneStatementPerLine")
124+
125+
// invalid noqa
126+
.containsIssue(17, "python:S1309").doesNotContainIssue(17, "python:PrintStatementUsage")
127+
.containsIssue(18, "python:S1309").containsIssue(18, "python:PrintStatementUsage")
128+
.containsIssue(19, "python:S1309").doesNotContainIssue(19, "python:PrintStatementUsage").doesNotContainIssue(19, "python:PrintStatementUsage")
129+
130+
// noqa at the end of file
131+
.containsIssue(21, "python:S1309")
132+
.containsIssue(22, "python:S1309").doesNotContainIssue(22, "python:PrintStatementUsage");
133+
}
134+
98135
private void analyzeProject(SonarScanner scanner) {
99136
String projectKey = scanner.getProperty("sonar.projectKey");
100137
ORCHESTRATOR.getServer().provisionProject(projectKey, projectKey);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 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 Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.sonar.check.Rule;
20+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
21+
import org.sonar.plugins.python.api.nosonar.NoSonarInfoParser;
22+
import org.sonar.plugins.python.api.tree.Token;
23+
import org.sonar.plugins.python.api.tree.Tree;
24+
import org.sonar.plugins.python.api.tree.Trivia;
25+
26+
@Rule(key = "S1309")
27+
public class NoQaCommentCheck extends PythonSubscriptionCheck {
28+
29+
private static final String MESSAGE = "Is #noqa used to exclude false-positive or to hide real quality flaw?";
30+
31+
@Override
32+
public void initialize(Context context) {
33+
context.registerSyntaxNodeConsumer(Tree.Kind.TOKEN, ctx -> {
34+
Token token = (Token) ctx.syntaxNode();
35+
for (Trivia trivia : token.trivia()) {
36+
String commentLine = trivia.token().value();
37+
if (NoSonarInfoParser.isValidNoQa(commentLine)) {
38+
ctx.addIssue(trivia.token(), MESSAGE);
39+
}
40+
}
41+
});
42+
}
43+
}

python-checks/src/main/java/org/sonar/python/checks/OpenSourceCheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ public Stream<Class<?>> getChecks() {
292292
NonStringInAllPropertyCheck.class,
293293
NonSingletonTfVariableCheck.class,
294294
NoPersonReferenceInTodoCheck.class,
295+
NoQaCommentCheck.class,
295296
NoReRaiseInExitCheck.class,
296297
NoSonarCommentCheck.class,
297298
NoSonarCommentFormatCheck.class,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>This rule allows you to track the usage of <code>noqa</code> comments used to suppress specific linting issues.</p>
3+
<h3>Noncompliant code example</h3>
4+
<pre>
5+
... # noqa: S100
6+
</pre>
7+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "Track uses of noqa comments",
3+
"type": "CODE_SMELL",
4+
"code": {
5+
"impacts": {
6+
"MAINTAINABILITY": "INFO"
7+
},
8+
"attribute": "CLEAR"
9+
},
10+
"status": "ready",
11+
"remediation": {
12+
"func": "Constant\/Issue",
13+
"constantCost": "10min"
14+
},
15+
"tags": [],
16+
"defaultSeverity": "Info",
17+
"ruleSpecification": "RSPEC-1309",
18+
"sqKey": "S1309",
19+
"scope": "All",
20+
"quickfix": "unknown"
21+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 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 Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class NoQaCommentCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/noqaComment.py", new NoQaCommentCheck());
27+
}
28+
29+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
def divide(numerator, denominator):
2+
# Noncompliant@+1 {{Is #noqa used to exclude false-positive or to hide real quality flaw?}}
3+
return numerator / denominator # noqa denominator value might be 0
4+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5+
6+
# Noncompliant@+1
7+
#noqa
8+
9+
# Noncompliant@+1
10+
# noqa: E501
11+
12+
# Noncompliant@+1
13+
# noqa: E501,W503
14+
15+
# a noqa just mixed in should not be detected
16+
17+
# NOQA should not be detected
18+
# NOSONAR should not be detected (different rule)
19+
20+
# NoQa in mixed case should not be detected (only lowercase noqa is valid)
21+
22+
# no qa with space should not be detected
23+
24+
# Invalid example
25+
# noqa:
26+
# Noncompliant@-1
27+
28+
# something noqa: something, not valid since not at the beginning of the comment
29+
30+
# this is not no qa
31+
a = "noqa-text"
32+
text = "noqa is mentioned here"

python-commons/src/main/java/org/sonar/plugins/python/MeasuresRepository.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ private void saveInternal(PythonInputFile inputFile, PythonVisitorContext visito
6565
FileMetrics fileMetrics = new FileMetrics(visitorContext, isNotebook(inputFile));
6666
FileLinesVisitor fileLinesVisitor = fileMetrics.fileLinesVisitor();
6767

68-
processNoSonarInFile(inputFile);
69-
7068
if (!isInSonarLint) {
7169
Set<Integer> linesOfCode = fileLinesVisitor.getLinesOfCode();
7270
saveMetricOnFile(inputFile, CoreMetrics.NCLOC, linesOfCode.size());
@@ -90,16 +88,6 @@ private void saveInternal(PythonInputFile inputFile, PythonVisitorContext visito
9088
}
9189
}
9290

93-
private void processNoSonarInFile(PythonInputFile inputFile) {
94-
try {
95-
lock.lock();
96-
var linesWithEmptyNosonar = noSonarLineInfoCollector.getLinesWithEmptyNoSonar(inputFile.wrappedFile().key());
97-
noSonarFilter.noSonarInFile(inputFile.wrappedFile(), linesWithEmptyNosonar);
98-
} finally {
99-
lock.unlock();
100-
}
101-
}
102-
10391
static boolean isNotebook(PythonInputFile inputFile) {
10492
return inputFile.kind() == PythonInputFile.Kind.IPYTHON;
10593
}

0 commit comments

Comments
 (0)