3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 package org.sonar.ce.task.projectanalysis.issue;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.List;
26 import java.util.Optional;
27 import java.util.stream.IntStream;
28 import org.apache.commons.codec.digest.DigestUtils;
29 import org.junit.Before;
30 import org.junit.Rule;
31 import org.junit.Test;
32 import org.sonar.api.config.Configuration;
33 import org.sonar.api.rule.RuleKey;
34 import org.sonar.api.rules.RuleType;
35 import org.sonar.ce.task.projectanalysis.component.Component;
36 import org.sonar.ce.task.projectanalysis.component.ReportComponent;
37 import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
38 import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository;
39 import org.sonar.core.issue.DefaultIssue;
40 import org.sonar.core.util.CloseableIterator;
41 import org.sonar.db.protobuf.DbCommons;
42 import org.sonar.db.protobuf.DbIssues;
43 import org.sonar.server.issue.TaintChecker;
45 import static org.assertj.core.api.Assertions.assertThat;
46 import static org.mockito.Mockito.mock;
47 import static org.mockito.Mockito.verify;
48 import static org.mockito.Mockito.verifyNoInteractions;
49 import static org.mockito.Mockito.verifyNoMoreInteractions;
50 import static org.mockito.Mockito.when;
52 public class ComputeLocationHashesVisitorTest {
53 private static final String EXAMPLE_LINE_OF_CODE_FORMAT = "int example = line + of + code + %d; ";
54 private static final String LINE_IN_THE_MAIN_FILE = "String string = 'line-in-the-main-file';";
55 private static final String ANOTHER_LINE_IN_THE_MAIN_FILE = "String string = 'another-line-in-the-main-file';";
56 private static final String LINE_IN_ANOTHER_FILE = "String string = 'line-in-the-another-file';";
58 private static final RuleKey RULE_KEY = RuleKey.of("javasecurity", "S001");
59 private static final Component FILE_1 = ReportComponent.builder(Component.Type.FILE, 2).build();
60 private static final Component FILE_2 = ReportComponent.builder(Component.Type.FILE, 3).build();
61 private static final Component ROOT = ReportComponent.builder(Component.Type.PROJECT, 1)
62 .addChildren(FILE_1, FILE_2)
65 private final SourceLinesRepository sourceLinesRepository = mock(SourceLinesRepository.class);
66 private final MutableConfiguration configuration = new MutableConfiguration();
67 private final TaintChecker taintChecker = new TaintChecker(configuration);
69 public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
70 private final ComputeLocationHashesVisitor underTest = new ComputeLocationHashesVisitor(taintChecker, sourceLinesRepository, treeRootHolder);
73 public void before() {
74 Iterator<String> stringIterator = IntStream.rangeClosed(1, 9)
75 .mapToObj(i -> String.format(EXAMPLE_LINE_OF_CODE_FORMAT, i))
77 when(sourceLinesRepository.readLines(FILE_1)).thenReturn(CloseableIterator.from(stringIterator));
78 when(sourceLinesRepository.readLines(FILE_2)).thenReturn(newOneLineIterator(LINE_IN_ANOTHER_FILE));
79 treeRootHolder.setRoot(ROOT);
83 public void do_nothing_if_issue_is_unchanged() {
84 DefaultIssue issue = createIssue()
85 .setLocationsChanged(false)
87 .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
89 underTest.beforeComponent(FILE_1);
90 underTest.onIssue(FILE_1, issue);
91 underTest.beforeCaching(FILE_1);
93 DbIssues.Locations locations = issue.getLocations();
94 assertThat(locations.getChecksum()).isEmpty();
95 verifyNoInteractions(sourceLinesRepository);
99 public void do_nothing_if_issue_is_not_taint_vulnerability() {
100 DefaultIssue issue = createIssue()
101 .setRuleKey(RuleKey.of("repo", "rule"))
102 .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
104 underTest.onIssue(FILE_1, issue);
105 underTest.beforeCaching(FILE_1);
107 DbIssues.Locations locations = issue.getLocations();
108 assertThat(locations.getChecksum()).isEmpty();
109 verifyNoInteractions(sourceLinesRepository);
113 public void calculates_hash_for_multiple_lines() {
114 DefaultIssue issue = createIssue()
115 .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
117 underTest.onIssue(FILE_1, issue);
118 underTest.beforeCaching(FILE_1);
120 assertLocationHashIsMadeOf(issue, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;");
124 public void calculates_hash_for_multiple_files() {
125 DefaultIssue issue1 = createIssue()
126 .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
127 DefaultIssue issue2 = createIssue()
128 .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 1, LINE_IN_ANOTHER_FILE.length())).build());
130 underTest.onIssue(FILE_1, issue1);
131 underTest.beforeCaching(FILE_1);
133 underTest.onIssue(FILE_2, issue2);
134 underTest.beforeCaching(FILE_2);
136 assertLocationHashIsMadeOf(issue1, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;");
137 assertLocationHashIsMadeOf(issue2, "Stringstring='line-in-the-another-file';");
141 public void calculates_hash_for_partial_line() {
142 DefaultIssue issue = createIssue()
143 .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 13, 1, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
145 underTest.onIssue(FILE_1, issue);
146 underTest.beforeCaching(FILE_1);
148 assertLocationHashIsMadeOf(issue, "line+of+code+1;");
152 public void calculates_hash_for_partial_multiple_lines() {
153 DefaultIssue issue = createIssue()
154 .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 13, 3, 11)).build());
156 underTest.onIssue(FILE_1, issue);
157 underTest.beforeCaching(FILE_1);
159 assertLocationHashIsMadeOf(issue, "line+of+code+1;intexample=line+of+code+2;intexample");
163 public void dont_calculate_hash_if_no_textRange() {
164 // primary location and one of the secondary locations have no text range
165 DefaultIssue issue = createIssue()
166 .setLocations(DbIssues.Locations.newBuilder()
167 .addFlow(DbIssues.Flow.newBuilder()
168 .addLocation(DbIssues.Location.newBuilder()
169 .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
170 .setComponentId(FILE_1.getUuid())
172 .addLocation(DbIssues.Location.newBuilder()
173 .setComponentId(FILE_2.getUuid())
178 when(sourceLinesRepository.readLines(FILE_1)).thenReturn(newOneLineIterator(LINE_IN_THE_MAIN_FILE));
180 underTest.onIssue(FILE_1, issue);
181 underTest.beforeCaching(FILE_1);
183 verify(sourceLinesRepository).readLines(FILE_1);
184 verifyNoMoreInteractions(sourceLinesRepository);
185 DbIssues.Locations locations = issue.getLocations();
186 assertThat(locations.getFlow(0).getLocation(0).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-main-file';"));
187 assertThat(locations.getFlow(0).getLocation(1).getChecksum()).isEmpty();
191 public void calculates_hash_for_multiple_locations() {
192 DefaultIssue issue = createIssue()
193 .setLocations(DbIssues.Locations.newBuilder()
194 .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
195 .addFlow(DbIssues.Flow.newBuilder()
196 .addLocation(DbIssues.Location.newBuilder()
197 .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
198 .setComponentId(FILE_1.getUuid())
200 .addLocation(DbIssues.Location.newBuilder()
201 .setTextRange(createRange(1, 0, 1, LINE_IN_ANOTHER_FILE.length()))
202 .setComponentId(FILE_2.getUuid())
207 when(sourceLinesRepository.readLines(FILE_1)).thenReturn(newOneLineIterator(LINE_IN_THE_MAIN_FILE));
208 when(sourceLinesRepository.readLines(FILE_2)).thenReturn(newOneLineIterator(LINE_IN_ANOTHER_FILE));
210 underTest.onIssue(FILE_1, issue);
211 underTest.beforeCaching(FILE_1);
213 DbIssues.Locations locations = issue.getLocations();
215 assertThat(locations.getFlow(0).getLocation(0).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-main-file';"));
216 assertThat(locations.getFlow(0).getLocation(1).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-another-file';"));
220 public void calculates_hash_for_multiple_locations_in_same_file() {
221 DefaultIssue issue = createIssue()
222 .setComponentUuid(FILE_1.getUuid())
223 .setLocations(DbIssues.Locations.newBuilder()
224 .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
225 .addFlow(DbIssues.Flow.newBuilder()
226 .addLocation(DbIssues.Location.newBuilder()
227 .setComponentId(FILE_1.getUuid())
228 .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
230 .addLocation(DbIssues.Location.newBuilder()
231 // component id can be empty if location is in the same file
232 .setTextRange(createRange(2, 0, 2, ANOTHER_LINE_IN_THE_MAIN_FILE.length()))
237 when(sourceLinesRepository.readLines(FILE_1)).thenReturn(manyLinesIterator(LINE_IN_THE_MAIN_FILE, ANOTHER_LINE_IN_THE_MAIN_FILE));
239 underTest.onIssue(FILE_1, issue);
240 underTest.beforeCaching(FILE_1);
242 DbIssues.Locations locations = issue.getLocations();
244 assertThat(locations.getFlow(0).getLocation(0).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-main-file';"));
245 assertThat(locations.getFlow(0).getLocation(1).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='another-line-in-the-main-file';"));
249 public void beforeCaching_whenSecurityHotspots_shouldCalculateHashForPrimaryLocation() {
250 DefaultIssue issue = createIssue()
251 .setRuleKey(RuleKey.of("repo", "rule"))
252 .setType(RuleType.SECURITY_HOTSPOT)
253 .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
254 underTest.onIssue(FILE_1, issue);
256 underTest.beforeCaching(FILE_1);
258 assertLocationHashIsMadeOf(issue, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;");
262 public void beforeCaching_whenSecurityHotspots_shouldNotCalculateHashForSecondaryLocations() {
263 DefaultIssue issue = createIssue()
264 .setType(RuleType.SECURITY_HOTSPOT)
265 .setLocations(DbIssues.Locations.newBuilder()
266 .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
267 .addFlow(DbIssues.Flow.newBuilder()
268 .addLocation(DbIssues.Location.newBuilder()
269 .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
270 .setComponentId(FILE_1.getUuid())
272 .addLocation(DbIssues.Location.newBuilder()
273 .setTextRange(createRange(1, 0, 1, LINE_IN_ANOTHER_FILE.length()))
274 .setComponentId(FILE_2.getUuid())
278 when(sourceLinesRepository.readLines(FILE_1)).thenReturn(newOneLineIterator(LINE_IN_THE_MAIN_FILE));
279 when(sourceLinesRepository.readLines(FILE_2)).thenReturn(newOneLineIterator(LINE_IN_ANOTHER_FILE));
280 underTest.onIssue(FILE_1, issue);
282 underTest.beforeCaching(FILE_1);
284 DbIssues.Locations locations = issue.getLocations();
285 assertLocationHashIsMadeOf(issue, "Stringstring='line-in-the-main-file';");
286 assertThat(locations.getFlow(0).getLocation(0).getChecksum()).isEmpty();
287 assertThat(locations.getFlow(0).getLocation(1).getChecksum()).isEmpty();
290 private DbCommons.TextRange createRange(int startLine, int startOffset, int endLine, int endOffset) {
291 return DbCommons.TextRange.newBuilder()
292 .setStartLine(startLine).setStartOffset(startOffset)
293 .setEndLine(endLine).setEndOffset(endOffset)
297 private DefaultIssue createIssue() {
298 return new DefaultIssue()
299 .setLocationsChanged(true)
300 .setRuleKey(RULE_KEY)
301 .setIsFromExternalRuleEngine(false)
302 .setType(RuleType.CODE_SMELL);
305 private void assertLocationHashIsMadeOf(DefaultIssue issue, String stringToHash) {
306 String expectedHash = DigestUtils.md5Hex(stringToHash);
307 DbIssues.Locations locations = issue.getLocations();
308 assertThat(locations.getChecksum()).isEqualTo(expectedHash);
311 private CloseableIterator<String> newOneLineIterator(String lineContent) {
312 return CloseableIterator.from(List.of(lineContent).iterator());
315 private CloseableIterator<String> manyLinesIterator(String... lines) {
316 return CloseableIterator.from(List.of(lines).iterator());
319 private static class MutableConfiguration implements Configuration {
320 private final Map<String, String> keyValues = new HashMap<>();
322 public Configuration put(String key, String value) {
323 keyValues.put(key, value.trim());
328 public Optional<String> get(String key) {
329 return Optional.ofNullable(keyValues.get(key));
333 public boolean hasKey(String key) {
334 return keyValues.containsKey(key);
338 public String[] getStringArray(String key) {
339 throw new UnsupportedOperationException("getStringArray not implemented");