]> source.dussan.org Git - sonarqube.git/blob
e764a9d9b4e023ed04f018f3c809b784b794cf50
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 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.ce.task.projectanalysis.issue;
21
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map;
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;
44
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;
51
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';";
57
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)
63     .build();
64
65   private final SourceLinesRepository sourceLinesRepository = mock(SourceLinesRepository.class);
66   private final MutableConfiguration configuration = new MutableConfiguration();
67   private final TaintChecker taintChecker = new TaintChecker(configuration);
68   @Rule
69   public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
70   private final ComputeLocationHashesVisitor underTest = new ComputeLocationHashesVisitor(taintChecker, sourceLinesRepository, treeRootHolder);
71
72   @Before
73   public void before() {
74     Iterator<String> stringIterator = IntStream.rangeClosed(1, 9)
75       .mapToObj(i -> String.format(EXAMPLE_LINE_OF_CODE_FORMAT, i))
76       .iterator();
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);
80   }
81
82   @Test
83   public void do_nothing_if_issue_is_unchanged() {
84     DefaultIssue issue = createIssue()
85       .setLocationsChanged(false)
86       .setNew(false)
87       .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
88
89     underTest.beforeComponent(FILE_1);
90     underTest.onIssue(FILE_1, issue);
91     underTest.beforeCaching(FILE_1);
92
93     DbIssues.Locations locations = issue.getLocations();
94     assertThat(locations.getChecksum()).isEmpty();
95     verifyNoInteractions(sourceLinesRepository);
96   }
97
98   @Test
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());
103
104     underTest.onIssue(FILE_1, issue);
105     underTest.beforeCaching(FILE_1);
106
107     DbIssues.Locations locations = issue.getLocations();
108     assertThat(locations.getChecksum()).isEmpty();
109     verifyNoInteractions(sourceLinesRepository);
110   }
111
112   @Test
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());
116
117     underTest.onIssue(FILE_1, issue);
118     underTest.beforeCaching(FILE_1);
119
120     assertLocationHashIsMadeOf(issue, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;");
121   }
122
123   @Test
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());
129
130     underTest.onIssue(FILE_1, issue1);
131     underTest.beforeCaching(FILE_1);
132
133     underTest.onIssue(FILE_2, issue2);
134     underTest.beforeCaching(FILE_2);
135
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';");
138   }
139
140   @Test
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());
144
145     underTest.onIssue(FILE_1, issue);
146     underTest.beforeCaching(FILE_1);
147
148     assertLocationHashIsMadeOf(issue, "line+of+code+1;");
149   }
150
151   @Test
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());
155
156     underTest.onIssue(FILE_1, issue);
157     underTest.beforeCaching(FILE_1);
158
159     assertLocationHashIsMadeOf(issue, "line+of+code+1;intexample=line+of+code+2;intexample");
160   }
161
162   @Test
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())
171             .build())
172           .addLocation(DbIssues.Location.newBuilder()
173             .setComponentId(FILE_2.getUuid())
174             .build())
175           .build())
176         .build());
177
178     when(sourceLinesRepository.readLines(FILE_1)).thenReturn(newOneLineIterator(LINE_IN_THE_MAIN_FILE));
179
180     underTest.onIssue(FILE_1, issue);
181     underTest.beforeCaching(FILE_1);
182
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();
188   }
189
190   @Test
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())
199             .build())
200           .addLocation(DbIssues.Location.newBuilder()
201             .setTextRange(createRange(1, 0, 1, LINE_IN_ANOTHER_FILE.length()))
202             .setComponentId(FILE_2.getUuid())
203             .build())
204           .build())
205         .build());
206
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));
209
210     underTest.onIssue(FILE_1, issue);
211     underTest.beforeCaching(FILE_1);
212
213     DbIssues.Locations locations = issue.getLocations();
214
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';"));
217   }
218
219   @Test
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()))
229             .build())
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()))
233             .build())
234           .build())
235         .build());
236
237     when(sourceLinesRepository.readLines(FILE_1)).thenReturn(manyLinesIterator(LINE_IN_THE_MAIN_FILE, ANOTHER_LINE_IN_THE_MAIN_FILE));
238
239     underTest.onIssue(FILE_1, issue);
240     underTest.beforeCaching(FILE_1);
241
242     DbIssues.Locations locations = issue.getLocations();
243
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';"));
246   }
247
248   @Test
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);
255
256     underTest.beforeCaching(FILE_1);
257
258     assertLocationHashIsMadeOf(issue, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;");
259   }
260
261   @Test
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())
271             .build())
272           .addLocation(DbIssues.Location.newBuilder()
273             .setTextRange(createRange(1, 0, 1, LINE_IN_ANOTHER_FILE.length()))
274             .setComponentId(FILE_2.getUuid())
275             .build())
276           .build())
277         .build());
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);
281
282     underTest.beforeCaching(FILE_1);
283
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();
288   }
289
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)
294       .build();
295   }
296
297   private DefaultIssue createIssue() {
298     return new DefaultIssue()
299       .setLocationsChanged(true)
300       .setRuleKey(RULE_KEY)
301       .setIsFromExternalRuleEngine(false)
302       .setType(RuleType.CODE_SMELL);
303   }
304
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);
309   }
310
311   private CloseableIterator<String> newOneLineIterator(String lineContent) {
312     return CloseableIterator.from(List.of(lineContent).iterator());
313   }
314
315   private CloseableIterator<String> manyLinesIterator(String... lines) {
316     return CloseableIterator.from(List.of(lines).iterator());
317   }
318
319   private static class MutableConfiguration implements Configuration {
320     private final Map<String, String> keyValues = new HashMap<>();
321
322     public Configuration put(String key, String value) {
323       keyValues.put(key, value.trim());
324       return this;
325     }
326
327     @Override
328     public Optional<String> get(String key) {
329       return Optional.ofNullable(keyValues.get(key));
330     }
331
332     @Override
333     public boolean hasKey(String key) {
334       return keyValues.containsKey(key);
335     }
336
337     @Override
338     public String[] getStringArray(String key) {
339       throw new UnsupportedOperationException("getStringArray not implemented");
340     }
341   }
342 }