Skip to content

Commit 6690d95

Browse files
SONARPY-1231 Cache CPD tokens (#1325)
1 parent 227bdbb commit 6690d95

File tree

11 files changed

+589
-17
lines changed

11 files changed

+589
-17
lines changed

its/ruling/src/test/java/org/sonar/python/it/PythonPrAnalysisTest.java

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
import static java.nio.charset.StandardCharsets.UTF_8;
4545
import static org.assertj.core.api.Assertions.assertThat;
46+
import static org.sonar.python.it.RulingHelper.getMeasure;
4647
import static org.sonar.python.it.RulingHelper.getOrchestrator;
4748

4849
@RunWith(Parameterized.class)
@@ -53,6 +54,8 @@ public class PythonPrAnalysisTest {
5354

5455
private static final String PR_ANALYSIS_PROJECT_KEY = "prAnalysis";
5556
private static final String INCREMENTAL_ANALYSIS_PROFILE = "incrementalPrAnalysis";
57+
private static final String PR_KEY = "1";
58+
private static final String PR_BRANCH_NAME = "incremental";
5659

5760
@Rule
5861
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -62,13 +65,15 @@ public class PythonPrAnalysisTest {
6265
private final int expectedRecomputed;
6366
private final int expectedSkipped;
6467
private final List<String> deletedFiles;
68+
private final Integer expectedDuplicatedLines;
6569

66-
public PythonPrAnalysisTest(String scenario, int expectedTotalFiles, int expectedRecomputed, int expectedSkipped, List<String> deletedFiles) {
70+
public PythonPrAnalysisTest(String scenario, int expectedTotalFiles, int expectedRecomputed, int expectedSkipped, List<String> deletedFiles, Integer expectedDuplication) {
6771
this.scenario = scenario;
6872
this.expectedTotalFiles = expectedTotalFiles;
6973
this.expectedRecomputed = expectedRecomputed;
7074
this.expectedSkipped = expectedSkipped;
7175
this.deletedFiles = deletedFiles;
76+
this.expectedDuplicatedLines = expectedDuplication;
7277
}
7378

7479
@BeforeClass
@@ -83,15 +88,16 @@ public static void prepare_quality_profile() throws IOException {
8388

8489
@Parameters(name = "{index}: {0}")
8590
public static Collection<Object[]> data() {
86-
return List.of(new Object[][] {
87-
// {<scenario>, <total files>, <recomputed>, <skipped>, <deleted>}
88-
{"newFile", 10, 1, 9, Collections.emptyList()},
89-
{"changeInImportedModule", 9, 1, 7, Collections.emptyList()},
90-
{"changeInParent", 9, 1, 6, Collections.emptyList()},
91-
{"changeInPackageInit", 9, 1, 7, Collections.emptyList()},
92-
{"changeInRelativeImport", 9, 2, 4, Collections.emptyList()},
93-
{"deletedFile", 8, 0, 7, List.of("submodule.py")}}
94-
);
91+
return List.of(new Object[][]{
92+
// {<scenario>, <total files>, <recomputed>, <skipped>, <deleted>, <duplication on index.py>}
93+
{"newFile", 10, 1, 9, Collections.emptyList(), null},
94+
{"changeInImportedModule", 9, 1, 7, Collections.emptyList(), null},
95+
{"changeInParent", 9, 1, 6, Collections.emptyList(), null},
96+
{"changeInPackageInit", 9, 1, 7, Collections.emptyList(), null},
97+
{"changeInRelativeImport", 9, 2, 4, Collections.emptyList(), null},
98+
{"deletedFile", 8, 0, 7, List.of("submodule.py"), null},
99+
{"duplication", 10, 1, 9, Collections.emptyList(), 55}
100+
});
95101
}
96102

97103
@Test
@@ -105,11 +111,12 @@ public void pr_analysis_logs() throws IOException {
105111
// Analyze the changed branch
106112
setUpChanges(tempDirectory, scenario);
107113
SonarScanner build = prepareScanner(tempDirectory, PR_ANALYSIS_PROJECT_KEY, scenario, litsDifferencesFile)
108-
.setProperty("sonar.pullrequest.key", "1")
109-
.setProperty("sonar.pullrequest.branch", "incremental");
114+
.setProperty("sonar.pullrequest.key", PR_KEY)
115+
.setProperty("sonar.pullrequest.branch", PR_BRANCH_NAME);
110116

111117
BuildResult result = ORCHESTRATOR.executeBuild(build);
112118
assertPrAnalysisLogs(result);
119+
assertMeasures();
113120
}
114121

115122
@Test
@@ -153,6 +160,16 @@ private void assertPrAnalysisLogs(BuildResult result) {
153160
.contains(expectedFinalLog);
154161
}
155162

163+
private void assertMeasures() {
164+
if (expectedDuplicatedLines != null) {
165+
var duplicatedLines = getMeasure(ORCHESTRATOR, PR_KEY, PR_ANALYSIS_PROJECT_KEY + ":index_duplicate.py", "duplicated_lines");
166+
assertThat(duplicatedLines)
167+
.isNotNull();
168+
assertThat(Integer.parseInt(duplicatedLines.getValue()))
169+
.isEqualTo(expectedDuplicatedLines);
170+
}
171+
}
172+
156173
private void analyzeAndAssertBaseCommit(File tempFile, File litsDifferencesFile) throws IOException {
157174
FileUtils.copyDirectory(new File("../sources_pr_analysis", "baseCommit"), tempFile);
158175

@@ -178,7 +195,6 @@ private SonarScanner prepareScanner(File path, String projectKey, String scenari
178195
.setSourceDirs(".")
179196
.setProperty("sonar.lits.dump.old", FileLocation.of("src/test/resources/expected_pr_analysis/" + scenario).getFile().getAbsolutePath())
180197
.setProperty("sonar.lits.dump.new", FileLocation.of("target/actual").getFile().getAbsolutePath())
181-
.setProperty("sonar.cpd.exclusions", "**/*")
182198
.setProperty("sonar.lits.differences", litsDifferencesFile.getAbsolutePath())
183199
.setProperty("sonar.internal.analysis.failFast", "true")
184200
.setEnvironmentVariable("SONAR_RUNNER_OPTS", "-Xmx2000m");

its/ruling/src/test/java/org/sonar/python/it/RulingHelper.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
import java.nio.file.Files;
3030
import java.util.Arrays;
3131
import java.util.List;
32+
import org.sonarqube.ws.Measures;
33+
import org.sonarqube.ws.client.HttpConnector;
34+
import org.sonarqube.ws.client.WsClient;
35+
import org.sonarqube.ws.client.WsClientFactories;
36+
import org.sonarqube.ws.client.measures.ComponentRequest;
37+
38+
import static java.lang.Double.parseDouble;
39+
import static java.util.Collections.singletonList;
3240

3341
public class RulingHelper {
3442

@@ -139,4 +147,24 @@ static List<String> bugRuleKeys() {
139147
"S930"
140148
);
141149
}
150+
151+
static Measures.Measure getMeasure(Orchestrator orchestrator, String branch, String componentKey, String metricKey) {
152+
List<Measures.Measure> measures = getMeasures(orchestrator, branch, componentKey, singletonList(metricKey));
153+
return measures != null && measures.size() == 1 ? measures.get(0) : null;
154+
}
155+
156+
private static List<Measures.Measure> getMeasures(Orchestrator orchestrator, String prKey, String componentKey, List<String> metricKeys) {
157+
Measures.ComponentWsResponse response = newWsClient(orchestrator).measures().component(new ComponentRequest()
158+
.setComponent(componentKey)
159+
.setPullRequest(prKey)
160+
.setMetricKeys(metricKeys));
161+
return response.getComponent().getMeasuresList();
162+
}
163+
164+
static WsClient newWsClient(Orchestrator orchestrator) {
165+
return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
166+
.url(orchestrator.getServer().getUrl())
167+
.credentials(null, null)
168+
.build());
169+
}
142170
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'prAnalysis:index_duplicate.py':[
3+
9,
4+
19,
5+
29,
6+
39,
7+
49
8+
],
9+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from submodule import SubmoduleException
2+
from subpackage.parent import ParentException
3+
from subpackage import SubpackageException
4+
from relative_imports.module import RelativeImportException
5+
from relative_imports.sub.inner import RelativeImportSubpackageException
6+
7+
try:
8+
raise SubmoduleException()
9+
except (SubmoduleException, NotImplementedError): # Noncompliant
10+
print("Foo")
11+
12+
try:
13+
raise SubmoduleException()
14+
except (SubmoduleException, ValueError): # OK
15+
print("Foo")
16+
17+
try:
18+
raise ParentException()
19+
except (ParentException, NotImplementedError): # Noncompliant
20+
print("Foo")
21+
22+
try:
23+
raise ParentException()
24+
except (ParentException, ValueError): # OK
25+
print("Foo")
26+
27+
try:
28+
raise SubpackageException()
29+
except (SubpackageException, NotImplementedError): # Noncompliant
30+
print("Foo")
31+
32+
try:
33+
raise SubpackageException()
34+
except (SubpackageException, ValueError): # OK
35+
print("Foo")
36+
37+
try:
38+
raise RelativeImportException()
39+
except (RelativeImportException, NotImplementedError): # Noncompliant
40+
print("Foo")
41+
42+
try:
43+
raise RelativeImportException()
44+
except (RelativeImportException, ValueError): # OK
45+
print("Foo")
46+
47+
try:
48+
raise RelativeImportSubpackageException()
49+
except (RelativeImportSubpackageException, NotImplementedError): # Noncompliant
50+
print("Foo")
51+
52+
try:
53+
raise RelativeImportSubpackageException()
54+
except (RelativeImportSubpackageException, ValueError): # OK
55+
print("Foo")
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.caching;
21+
22+
import java.io.ByteArrayInputStream;
23+
import java.io.ByteArrayOutputStream;
24+
import java.io.IOException;
25+
import java.io.ObjectInputStream;
26+
import java.io.ObjectOutputStream;
27+
import java.io.Serializable;
28+
import java.util.List;
29+
import java.util.stream.Collectors;
30+
import org.sonar.plugins.python.api.tree.Token;
31+
import org.sonar.python.TokenLocation;
32+
33+
public class CpdSerializer {
34+
35+
private CpdSerializer() {
36+
// Prevent instantiation
37+
}
38+
39+
public static final class TokenInfo implements Serializable {
40+
public final int startLine;
41+
public final int startLineOffset;
42+
public final int endLine;
43+
public final int endLineOffset;
44+
public final String value;
45+
46+
public static TokenInfo from(Token token) {
47+
TokenLocation location = new TokenLocation(token);
48+
return new TokenInfo(location.startLine(), location.startLineOffset(), location.endLine(), location.endLineOffset(), token.value());
49+
}
50+
51+
public TokenInfo(int startLine, int startLineOffset, int endLine, int endLineOffset, String value) {
52+
this.startLine = startLine;
53+
this.startLineOffset = startLineOffset;
54+
this.endLine = endLine;
55+
this.endLineOffset = endLineOffset;
56+
this.value = value;
57+
}
58+
}
59+
60+
public static byte[] toBytes(List<Token> tokens) throws IOException {
61+
List<TokenInfo> tokenInfos = tokens.stream()
62+
.map(TokenInfo::from)
63+
.collect(Collectors.toList());
64+
65+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
66+
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
67+
objectOutputStream.writeObject(tokenInfos);
68+
69+
return byteArrayOutputStream.toByteArray();
70+
}
71+
72+
public static List<TokenInfo> fromBytes(byte[] bytes) throws IOException, ClassNotFoundException {
73+
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
74+
return (List<TokenInfo>) objectInputStream.readObject();
75+
}
76+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.caching;
21+
22+
import java.io.IOException;
23+
import java.net.URI;
24+
import java.util.List;
25+
import org.junit.Test;
26+
import org.sonar.plugins.python.api.tree.Token;
27+
import org.sonar.python.api.PythonKeyword;
28+
import org.sonar.python.tree.TokenImpl;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
public class CpdSerializerTest {
33+
34+
@Test
35+
public void to_bytes_from_bytes() throws IOException, ClassNotFoundException {
36+
var sslrToken = com.sonar.sslr.api.Token.builder()
37+
.setLine(1)
38+
.setColumn(0)
39+
.setValueAndOriginalValue("pass")
40+
.setURI(URI.create(""))
41+
.setType(PythonKeyword.PASS)
42+
.build();
43+
44+
List<Token> tokens = List.of(new TokenImpl(sslrToken));
45+
byte[] bytes = CpdSerializer.toBytes(tokens);
46+
47+
List<CpdSerializer.TokenInfo> tokenInfos = CpdSerializer.fromBytes(bytes);
48+
49+
assertThat(tokenInfos)
50+
.hasSize(1);
51+
assertThat(tokenInfos.get(0))
52+
.usingRecursiveComparison().isEqualTo(new CpdSerializer.TokenInfo(1, 0, 1, 4, "pass"));
53+
}
54+
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ public boolean scanFileWithoutParsing(InputFile inputFile) {
152152
return false;
153153
}
154154
}
155-
return true;
155+
156+
return restoreAndPushMeasuresIfApplicable(inputFile);
156157
}
157158

158159
@Override
@@ -296,6 +297,14 @@ private void saveMeasures(InputFile inputFile, PythonVisitorContext visitorConte
296297
}
297298
}
298299

300+
private boolean restoreAndPushMeasuresIfApplicable(InputFile inputFile) {
301+
if (inputFile.type() == InputFile.Type.TEST) {
302+
return true;
303+
}
304+
305+
return cpdAnalyzer.pushCachedCpdTokens(inputFile, indexer.cacheContext());
306+
}
307+
299308
private void saveMetricOnFile(InputFile inputFile, Metric<Integer> metric, Integer value) {
300309
context.<Integer>newMeasure()
301310
.withValue(value)

sonar-python-plugin/src/main/java/org/sonar/plugins/python/caching/Caching.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class Caching {
4545
public static final String PROJECT_FILES_KEY = "python:files";
4646
public static final String TYPESHED_MODULES_KEY = "python:typeshed_modules";
4747
public static final String CACHE_VERSION_KEY = "python:cache_version";
48+
public static final String CPD_TOKENS_CACHE_KEY_PREFIX = "python:cpd:data:";
4849

4950
private static final Logger LOG = Loggers.get(Caching.class);
5051

0 commit comments

Comments
 (0)