You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ComputeLocationHashesVisitor.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 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. import java.util.HashMap;
  22. import java.util.LinkedList;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.regex.Pattern;
  26. import org.apache.commons.codec.digest.DigestUtils;
  27. import org.slf4j.Logger;
  28. import org.slf4j.LoggerFactory;
  29. import org.sonar.ce.task.projectanalysis.component.Component;
  30. import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
  31. import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository;
  32. import org.sonar.core.issue.DefaultIssue;
  33. import org.sonar.core.util.CloseableIterator;
  34. import org.sonar.db.protobuf.DbCommons;
  35. import org.sonar.db.protobuf.DbIssues;
  36. import org.sonar.server.issue.TaintChecker;
  37. import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
  38. /**
  39. * This visitor will update the locations field of issues, by filling hashes for their locations:
  40. * - Primary location hash: for all issues, when needed (ie. is missing or the issue is new/updated)
  41. * - Secondary location hash: only for taint vulnerabilities and security hotspots, when needed (the issue is new/updated)
  42. * For performance reasons, it will read each source code file once and feed the lines to all locations in that file.
  43. */
  44. public class ComputeLocationHashesVisitor extends IssueVisitor {
  45. private static final Logger LOGGER = LoggerFactory.getLogger(ComputeLocationHashesVisitor.class);
  46. private static final Pattern MATCH_ALL_WHITESPACES = Pattern.compile("\\s");
  47. private final List<DefaultIssue> issuesForAllLocations = new LinkedList<>();
  48. private final List<DefaultIssue> issuesForPrimaryLocation = new LinkedList<>();
  49. private final SourceLinesRepository sourceLinesRepository;
  50. private final TreeRootHolder treeRootHolder;
  51. private final TaintChecker taintChecker;
  52. public ComputeLocationHashesVisitor(TaintChecker taintChecker, SourceLinesRepository sourceLinesRepository, TreeRootHolder treeRootHolder) {
  53. this.taintChecker = taintChecker;
  54. this.sourceLinesRepository = sourceLinesRepository;
  55. this.treeRootHolder = treeRootHolder;
  56. }
  57. @Override
  58. public void beforeComponent(Component component) {
  59. issuesForAllLocations.clear();
  60. issuesForPrimaryLocation.clear();
  61. }
  62. @Override
  63. public void onIssue(Component component, DefaultIssue issue) {
  64. if (issueNeedsLocationHashes(issue)) {
  65. if (shouldComputeAllLocationHashes(issue)) {
  66. issuesForAllLocations.add(issue);
  67. } else if (shouldComputePrimaryLocationHash(issue)) {
  68. // Issues in this situation are not necessarily marked as changed, so we do it to ensure persistence
  69. issue.setChanged(true);
  70. issuesForPrimaryLocation.add(issue);
  71. }
  72. }
  73. }
  74. private static boolean issueNeedsLocationHashes(DefaultIssue issue) {
  75. DbIssues.Locations locations = issue.getLocations();
  76. return !issue.isFromExternalRuleEngine()
  77. && !issue.isBeingClosed()
  78. && locations != null;
  79. }
  80. private boolean shouldComputeAllLocationHashes(DefaultIssue issue) {
  81. return taintChecker.isTaintVulnerability(issue)
  82. && isIssueUpdated(issue);
  83. }
  84. private static boolean shouldComputePrimaryLocationHash(DefaultIssue issue) {
  85. DbIssues.Locations locations = issue.getLocations();
  86. return (locations.hasTextRange() && !locations.hasChecksum())
  87. || isIssueUpdated(issue);
  88. }
  89. private static boolean isIssueUpdated(DefaultIssue issue) {
  90. return issue.isNew() || issue.locationsChanged();
  91. }
  92. @Override
  93. public void beforeCaching(Component component) {
  94. Map<Component, List<Location>> locationsByComponent = new HashMap<>();
  95. List<LocationToSet> locationsToSet = new LinkedList<>();
  96. // Issues that needs both primary and secondary locations hashes
  97. extractForAllLocations(component, locationsByComponent, locationsToSet);
  98. // Then issues that needs only primary locations
  99. extractForPrimaryLocation(component, locationsByComponent, locationsToSet);
  100. // Feed lines to locations, component by component
  101. locationsByComponent.forEach(this::updateLocationsInComponent);
  102. // Finalize by setting hashes
  103. locationsByComponent.values().forEach(list -> list.forEach(Location::afterAllLines));
  104. // set new locations to issues
  105. locationsToSet.forEach(LocationToSet::set);
  106. issuesForAllLocations.clear();
  107. issuesForPrimaryLocation.clear();
  108. }
  109. private void extractForAllLocations(Component component, Map<Component, List<Location>> locationsByComponent, List<LocationToSet> locationsToSet) {
  110. for (DefaultIssue issue : issuesForAllLocations) {
  111. DbIssues.Locations.Builder locationsBuilder = ((DbIssues.Locations) issue.getLocations()).toBuilder();
  112. addPrimaryLocation(component, locationsByComponent, locationsBuilder);
  113. addSecondaryLocations(issue, locationsByComponent, locationsBuilder);
  114. locationsToSet.add(new LocationToSet(issue, locationsBuilder));
  115. }
  116. }
  117. private void extractForPrimaryLocation(Component component, Map<Component, List<Location>> locationsByComponent, List<LocationToSet> locationsToSet) {
  118. for (DefaultIssue issue : issuesForPrimaryLocation) {
  119. DbIssues.Locations.Builder locationsBuilder = ((DbIssues.Locations) issue.getLocations()).toBuilder();
  120. addPrimaryLocation(component, locationsByComponent, locationsBuilder);
  121. locationsToSet.add(new LocationToSet(issue, locationsBuilder));
  122. }
  123. }
  124. private static void addPrimaryLocation(Component component, Map<Component, List<Location>> locationsByComponent, DbIssues.Locations.Builder locationsBuilder) {
  125. if (locationsBuilder.hasTextRange()) {
  126. PrimaryLocation primaryLocation = new PrimaryLocation(locationsBuilder);
  127. locationsByComponent.computeIfAbsent(component, c -> new LinkedList<>()).add(primaryLocation);
  128. }
  129. }
  130. private void addSecondaryLocations(DefaultIssue issue, Map<Component, List<Location>> locationsByComponent, DbIssues.Locations.Builder locationsBuilder) {
  131. List<DbIssues.Location.Builder> locationBuilders = locationsBuilder.getFlowBuilderList().stream()
  132. .flatMap(flowBuilder -> flowBuilder.getLocationBuilderList().stream())
  133. .filter(DbIssues.Location.Builder::hasTextRange)
  134. .toList();
  135. locationBuilders.forEach(locationBuilder -> addSecondaryLocation(locationBuilder, issue, locationsByComponent));
  136. }
  137. private void addSecondaryLocation(DbIssues.Location.Builder locationBuilder, DefaultIssue issue, Map<Component, List<Location>> locationsByComponent) {
  138. String componentUuid = defaultIfEmpty(locationBuilder.getComponentId(), issue.componentUuid());
  139. Component locationComponent = treeRootHolder.getComponentByUuid(componentUuid);
  140. locationsByComponent.computeIfAbsent(locationComponent, c -> new LinkedList<>()).add(new SecondaryLocation(locationBuilder));
  141. }
  142. private void updateLocationsInComponent(Component component, List<Location> locations) {
  143. try (CloseableIterator<String> linesIterator = sourceLinesRepository.readLines(component)) {
  144. int lineNumber = 1;
  145. while (linesIterator.hasNext()) {
  146. String line = linesIterator.next();
  147. for (Location location : locations) {
  148. location.processLine(lineNumber, line);
  149. }
  150. lineNumber++;
  151. }
  152. }
  153. }
  154. private static class LocationToSet {
  155. private final DefaultIssue issue;
  156. private final DbIssues.Locations.Builder locationsBuilder;
  157. public LocationToSet(DefaultIssue issue, DbIssues.Locations.Builder locationsBuilder) {
  158. this.issue = issue;
  159. this.locationsBuilder = locationsBuilder;
  160. }
  161. void set() {
  162. issue.setLocations(locationsBuilder.build());
  163. }
  164. }
  165. private static class PrimaryLocation extends Location {
  166. private final DbIssues.Locations.Builder locationsBuilder;
  167. public PrimaryLocation(DbIssues.Locations.Builder locationsBuilder) {
  168. this.locationsBuilder = locationsBuilder;
  169. }
  170. @Override
  171. DbCommons.TextRange getTextRange() {
  172. return locationsBuilder.getTextRange();
  173. }
  174. @Override
  175. void setHash(String hash) {
  176. locationsBuilder.setChecksum(hash);
  177. }
  178. }
  179. private static class SecondaryLocation extends Location {
  180. private final DbIssues.Location.Builder locationBuilder;
  181. public SecondaryLocation(DbIssues.Location.Builder locationBuilder) {
  182. this.locationBuilder = locationBuilder;
  183. }
  184. @Override
  185. DbCommons.TextRange getTextRange() {
  186. return locationBuilder.getTextRange();
  187. }
  188. @Override
  189. void setHash(String hash) {
  190. locationBuilder.setChecksum(hash);
  191. }
  192. }
  193. private abstract static class Location {
  194. private final StringBuilder hashBuilder = new StringBuilder();
  195. abstract DbCommons.TextRange getTextRange();
  196. abstract void setHash(String hash);
  197. public void processLine(int lineNumber, String line) {
  198. DbCommons.TextRange textRange = getTextRange();
  199. if (lineNumber > textRange.getEndLine() || lineNumber < textRange.getStartLine()) {
  200. return;
  201. }
  202. try {
  203. if (lineNumber == textRange.getStartLine() && lineNumber == textRange.getEndLine()) {
  204. hashBuilder.append(line, textRange.getStartOffset(), textRange.getEndOffset());
  205. } else if (lineNumber == textRange.getStartLine()) {
  206. hashBuilder.append(line, textRange.getStartOffset(), line.length());
  207. } else if (lineNumber < textRange.getEndLine()) {
  208. hashBuilder.append(line);
  209. } else {
  210. hashBuilder.append(line, 0, textRange.getEndOffset());
  211. }
  212. } catch (IndexOutOfBoundsException e) {
  213. LOGGER.debug("Try to compute issue location hash from {} to {} on line ({} chars): {}",
  214. textRange.getStartOffset(), textRange.getEndOffset(), line.length(), line);
  215. }
  216. }
  217. void afterAllLines() {
  218. String issueContentWithoutWhitespaces = MATCH_ALL_WHITESPACES.matcher(hashBuilder.toString()).replaceAll("");
  219. String hash = DigestUtils.md5Hex(issueContentWithoutWhitespaces);
  220. setHash(hash);
  221. }
  222. }
  223. }