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.

IssueCounter.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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 com.google.common.collect.EnumMultiset;
  22. import com.google.common.collect.HashMultiset;
  23. import com.google.common.collect.ImmutableMap;
  24. import com.google.common.collect.Multiset;
  25. import java.util.HashMap;
  26. import java.util.Map;
  27. import javax.annotation.Nullable;
  28. import org.sonar.api.issue.IssueStatus;
  29. import org.sonar.api.issue.impact.Severity;
  30. import org.sonar.api.issue.impact.SoftwareQuality;
  31. import org.sonar.api.rules.RuleType;
  32. import org.sonar.ce.task.projectanalysis.component.Component;
  33. import org.sonar.ce.task.projectanalysis.measure.Measure;
  34. import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
  35. import org.sonar.ce.task.projectanalysis.metric.Metric;
  36. import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
  37. import org.sonar.core.issue.DefaultIssue;
  38. import org.sonar.server.measure.ImpactMeasureBuilder;
  39. import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
  40. import static org.sonar.api.issue.Issue.STATUS_OPEN;
  41. import static org.sonar.api.issue.Issue.STATUS_REOPENED;
  42. import static org.sonar.api.measures.CoreMetrics.ACCEPTED_ISSUES_KEY;
  43. import static org.sonar.api.measures.CoreMetrics.BLOCKER_VIOLATIONS_KEY;
  44. import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
  45. import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS_KEY;
  46. import static org.sonar.api.measures.CoreMetrics.CONFIRMED_ISSUES_KEY;
  47. import static org.sonar.api.measures.CoreMetrics.CRITICAL_VIOLATIONS_KEY;
  48. import static org.sonar.api.measures.CoreMetrics.FALSE_POSITIVE_ISSUES_KEY;
  49. import static org.sonar.api.measures.CoreMetrics.HIGH_IMPACT_ACCEPTED_ISSUES_KEY;
  50. import static org.sonar.api.measures.CoreMetrics.INFO_VIOLATIONS_KEY;
  51. import static org.sonar.api.measures.CoreMetrics.MAINTAINABILITY_ISSUES_KEY;
  52. import static org.sonar.api.measures.CoreMetrics.MAJOR_VIOLATIONS_KEY;
  53. import static org.sonar.api.measures.CoreMetrics.MINOR_VIOLATIONS_KEY;
  54. import static org.sonar.api.measures.CoreMetrics.NEW_ACCEPTED_ISSUES_KEY;
  55. import static org.sonar.api.measures.CoreMetrics.NEW_BLOCKER_VIOLATIONS_KEY;
  56. import static org.sonar.api.measures.CoreMetrics.NEW_BUGS_KEY;
  57. import static org.sonar.api.measures.CoreMetrics.NEW_CODE_SMELLS_KEY;
  58. import static org.sonar.api.measures.CoreMetrics.NEW_CRITICAL_VIOLATIONS_KEY;
  59. import static org.sonar.api.measures.CoreMetrics.NEW_INFO_VIOLATIONS_KEY;
  60. import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_ISSUES_KEY;
  61. import static org.sonar.api.measures.CoreMetrics.NEW_MAJOR_VIOLATIONS_KEY;
  62. import static org.sonar.api.measures.CoreMetrics.NEW_MINOR_VIOLATIONS_KEY;
  63. import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_ISSUES_KEY;
  64. import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_KEY;
  65. import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_ISSUES_KEY;
  66. import static org.sonar.api.measures.CoreMetrics.NEW_VIOLATIONS_KEY;
  67. import static org.sonar.api.measures.CoreMetrics.NEW_VULNERABILITIES_KEY;
  68. import static org.sonar.api.measures.CoreMetrics.OPEN_ISSUES_KEY;
  69. import static org.sonar.api.measures.CoreMetrics.RELIABILITY_ISSUES_KEY;
  70. import static org.sonar.api.measures.CoreMetrics.REOPENED_ISSUES_KEY;
  71. import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_KEY;
  72. import static org.sonar.api.measures.CoreMetrics.SECURITY_ISSUES_KEY;
  73. import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY;
  74. import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY;
  75. import static org.sonar.api.rule.Severity.BLOCKER;
  76. import static org.sonar.api.rule.Severity.CRITICAL;
  77. import static org.sonar.api.rule.Severity.INFO;
  78. import static org.sonar.api.rule.Severity.MAJOR;
  79. import static org.sonar.api.rule.Severity.MINOR;
  80. import static org.sonar.api.rules.RuleType.BUG;
  81. import static org.sonar.api.rules.RuleType.CODE_SMELL;
  82. import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
  83. import static org.sonar.api.rules.RuleType.VULNERABILITY;
  84. /**
  85. * For each component, computes the measures related to number of issues:
  86. * <ul>
  87. * <li>issues per status (open, reopen, confirmed)</li>
  88. * <li>issues per resolution (unresolved, false-positives, won't fix)</li>
  89. * <li>issues per severity (from info to blocker)</li>
  90. * <li>issues per type (code smell, bug, vulnerability, security hotspots)</li>
  91. * <li>issues per impact</li>
  92. * </ul>
  93. * For each value, the variation on configured periods is also computed.
  94. */
  95. public class IssueCounter extends IssueVisitor {
  96. private static final Map<String, String> SEVERITY_TO_METRIC_KEY = ImmutableMap.of(
  97. BLOCKER, BLOCKER_VIOLATIONS_KEY,
  98. CRITICAL, CRITICAL_VIOLATIONS_KEY,
  99. MAJOR, MAJOR_VIOLATIONS_KEY,
  100. MINOR, MINOR_VIOLATIONS_KEY,
  101. INFO, INFO_VIOLATIONS_KEY);
  102. private static final Map<String, String> SEVERITY_TO_NEW_METRIC_KEY = ImmutableMap.of(
  103. BLOCKER, NEW_BLOCKER_VIOLATIONS_KEY,
  104. CRITICAL, NEW_CRITICAL_VIOLATIONS_KEY,
  105. MAJOR, NEW_MAJOR_VIOLATIONS_KEY,
  106. MINOR, NEW_MINOR_VIOLATIONS_KEY,
  107. INFO, NEW_INFO_VIOLATIONS_KEY);
  108. static final Map<String, String> IMPACT_TO_METRIC_KEY = Map.of(
  109. SoftwareQuality.SECURITY.name(), SECURITY_ISSUES_KEY,
  110. SoftwareQuality.RELIABILITY.name(), RELIABILITY_ISSUES_KEY,
  111. SoftwareQuality.MAINTAINABILITY.name(), MAINTAINABILITY_ISSUES_KEY);
  112. static final Map<String, String> IMPACT_TO_NEW_METRIC_KEY = Map.of(
  113. SoftwareQuality.SECURITY.name(), NEW_SECURITY_ISSUES_KEY,
  114. SoftwareQuality.RELIABILITY.name(), NEW_RELIABILITY_ISSUES_KEY,
  115. SoftwareQuality.MAINTAINABILITY.name(), NEW_MAINTAINABILITY_ISSUES_KEY);
  116. private static final Map<RuleType, String> TYPE_TO_METRIC_KEY = ImmutableMap.<RuleType, String>builder()
  117. .put(CODE_SMELL, CODE_SMELLS_KEY)
  118. .put(BUG, BUGS_KEY)
  119. .put(VULNERABILITY, VULNERABILITIES_KEY)
  120. .put(SECURITY_HOTSPOT, SECURITY_HOTSPOTS_KEY)
  121. .build();
  122. private static final Map<RuleType, String> TYPE_TO_NEW_METRIC_KEY = ImmutableMap.<RuleType, String>builder()
  123. .put(CODE_SMELL, NEW_CODE_SMELLS_KEY)
  124. .put(BUG, NEW_BUGS_KEY)
  125. .put(VULNERABILITY, NEW_VULNERABILITIES_KEY)
  126. .put(SECURITY_HOTSPOT, NEW_SECURITY_HOTSPOTS_KEY)
  127. .build();
  128. private final MetricRepository metricRepository;
  129. private final MeasureRepository measureRepository;
  130. private final NewIssueClassifier newIssueClassifier;
  131. private final Map<String, Counters> countersByComponentUuid = new HashMap<>();
  132. private Counters currentCounters;
  133. public IssueCounter(MetricRepository metricRepository, MeasureRepository measureRepository, NewIssueClassifier newIssueClassifier) {
  134. this.metricRepository = metricRepository;
  135. this.measureRepository = measureRepository;
  136. this.newIssueClassifier = newIssueClassifier;
  137. }
  138. @Override
  139. public void beforeComponent(Component component) {
  140. currentCounters = new Counters();
  141. countersByComponentUuid.put(component.getUuid(), currentCounters);
  142. // aggregate children counters
  143. for (Component child : component.getChildren()) {
  144. Counters childCounters = countersByComponentUuid.remove(child.getUuid());
  145. currentCounters.add(childCounters);
  146. }
  147. }
  148. @Override
  149. public void onIssue(Component component, DefaultIssue issue) {
  150. currentCounters.add(issue);
  151. if (newIssueClassifier.isNew(component, issue)) {
  152. currentCounters.addOnPeriod(issue);
  153. }
  154. }
  155. @Override
  156. public void afterComponent(Component component) {
  157. addMeasuresBySeverity(component);
  158. addMeasuresByStatus(component);
  159. addMeasuresByType(component);
  160. addMeasuresByImpact(component);
  161. addNewMeasures(component);
  162. currentCounters = null;
  163. }
  164. private void addMeasuresBySeverity(Component component) {
  165. for (Map.Entry<String, String> entry : SEVERITY_TO_METRIC_KEY.entrySet()) {
  166. String severity = entry.getKey();
  167. String metricKey = entry.getValue();
  168. addMeasure(component, metricKey, currentCounters.counter().severityBag.count(severity));
  169. }
  170. }
  171. private void addMeasuresByStatus(Component component) {
  172. addMeasure(component, VIOLATIONS_KEY, currentCounters.counter().unresolved);
  173. addMeasure(component, OPEN_ISSUES_KEY, currentCounters.counter().open);
  174. addMeasure(component, REOPENED_ISSUES_KEY, currentCounters.counter().reopened);
  175. addMeasure(component, CONFIRMED_ISSUES_KEY, currentCounters.counter().confirmed);
  176. addMeasure(component, FALSE_POSITIVE_ISSUES_KEY, currentCounters.counter().falsePositives);
  177. addMeasure(component, ACCEPTED_ISSUES_KEY, currentCounters.counter().accepted);
  178. addMeasure(component, HIGH_IMPACT_ACCEPTED_ISSUES_KEY, currentCounters.counter().highImpactAccepted);
  179. }
  180. private void addMeasuresByImpact(Component component) {
  181. for (Map.Entry<String, Map<String, Long>> impactEntry : currentCounters.counter().impactsBag.entrySet()) {
  182. String json = ImpactMeasureBuilder.fromMap(impactEntry.getValue()).buildAsString();
  183. addMeasure(component, IMPACT_TO_METRIC_KEY.get(impactEntry.getKey()), json);
  184. }
  185. }
  186. private void addMeasuresByType(Component component) {
  187. for (Map.Entry<RuleType, String> entry : TYPE_TO_METRIC_KEY.entrySet()) {
  188. addMeasure(component, entry.getValue(), currentCounters.counter().typeBag.count(entry.getKey()));
  189. }
  190. }
  191. private void addMeasure(Component component, String metricKey, int value) {
  192. Metric metric = metricRepository.getByKey(metricKey);
  193. measureRepository.add(component, metric, Measure.newMeasureBuilder().create(value));
  194. }
  195. private void addMeasure(Component component, String metricKey, String data) {
  196. Metric metric = metricRepository.getByKey(metricKey);
  197. measureRepository.add(component, metric, Measure.newMeasureBuilder().create(data));
  198. }
  199. private void addNewMeasures(Component component) {
  200. if (!newIssueClassifier.isEnabled()) {
  201. return;
  202. }
  203. int unresolved = currentCounters.counterForPeriod().unresolved;
  204. measureRepository.add(component, metricRepository.getByKey(NEW_VIOLATIONS_KEY), Measure.newMeasureBuilder()
  205. .create(unresolved));
  206. for (Map.Entry<String, String> entry : SEVERITY_TO_NEW_METRIC_KEY.entrySet()) {
  207. String severity = entry.getKey();
  208. String metricKey = entry.getValue();
  209. Multiset<String> bag = currentCounters.counterForPeriod().severityBag;
  210. addMeasure(component, metricKey, bag.count(severity));
  211. }
  212. // waiting for Java 8 lambda in order to factor this loop with the previous one
  213. // (see call currentCounters.counterForPeriod(period.getIndex()).xxx with xxx as severityBag or typeBag)
  214. for (Map.Entry<RuleType, String> entry : TYPE_TO_NEW_METRIC_KEY.entrySet()) {
  215. RuleType type = entry.getKey();
  216. String metricKey = entry.getValue();
  217. Multiset<RuleType> bag = currentCounters.counterForPeriod().typeBag;
  218. addMeasure(component, metricKey, bag.count(type));
  219. }
  220. for (Map.Entry<String, Map<String, Long>> impactEntry : currentCounters.counterForPeriod().impactsBag.entrySet()) {
  221. String json = ImpactMeasureBuilder.fromMap(impactEntry.getValue()).buildAsString();
  222. addMeasure(component, IMPACT_TO_NEW_METRIC_KEY.get(impactEntry.getKey()), json);
  223. }
  224. addMeasure(component, NEW_ACCEPTED_ISSUES_KEY, currentCounters.counterForPeriod().accepted);
  225. }
  226. /**
  227. * Count issues by status, resolutions, rules, impacts and severities
  228. */
  229. private static class Counter {
  230. private int unresolved = 0;
  231. private int open = 0;
  232. private int reopened = 0;
  233. private int confirmed = 0;
  234. private int falsePositives = 0;
  235. private int accepted = 0;
  236. private int highImpactAccepted = 0;
  237. private final Multiset<String> severityBag = HashMultiset.create();
  238. /**
  239. * This map contains the number of issues per software quality along with their distribution based on (new) severity.
  240. */
  241. private final Map<String, Map<String, Long>> impactsBag = new HashMap<>();
  242. private final EnumMultiset<RuleType> typeBag = EnumMultiset.create(RuleType.class);
  243. public Counter() {
  244. initImpactsBag();
  245. }
  246. private void initImpactsBag() {
  247. for (SoftwareQuality quality : SoftwareQuality.values()) {
  248. impactsBag.put(quality.name(), ImpactMeasureBuilder.createEmpty().buildAsMap());
  249. }
  250. }
  251. void add(Counter counter) {
  252. unresolved += counter.unresolved;
  253. open += counter.open;
  254. reopened += counter.reopened;
  255. confirmed += counter.confirmed;
  256. falsePositives += counter.falsePositives;
  257. accepted += counter.accepted;
  258. highImpactAccepted += counter.highImpactAccepted;
  259. severityBag.addAll(counter.severityBag);
  260. typeBag.addAll(counter.typeBag);
  261. // Add impacts
  262. for (Map.Entry<String, Map<String, Long>> impactEntry : counter.impactsBag.entrySet()) {
  263. Map<String, Long> severityMap = impactsBag.get(impactEntry.getKey());
  264. for (Map.Entry<String, Long> severityEntry : impactEntry.getValue().entrySet()) {
  265. severityMap.compute(severityEntry.getKey(), (key, value) -> value + severityEntry.getValue());
  266. }
  267. }
  268. }
  269. void add(DefaultIssue issue) {
  270. if (issue.type() == SECURITY_HOTSPOT) {
  271. if (issue.resolution() == null) {
  272. typeBag.add(SECURITY_HOTSPOT);
  273. }
  274. return;
  275. }
  276. if (issue.resolution() == null) {
  277. unresolved++;
  278. typeBag.add(issue.type());
  279. severityBag.add(issue.severity());
  280. } else if (IssueStatus.FALSE_POSITIVE.equals(issue.issueStatus())) {
  281. falsePositives++;
  282. } else if (IssueStatus.ACCEPTED.equals(issue.issueStatus())) {
  283. accepted++;
  284. if (issue.impacts().values().stream().anyMatch(severity -> severity == Severity.HIGH)) {
  285. highImpactAccepted++;
  286. }
  287. }
  288. switch (issue.status()) {
  289. case STATUS_OPEN:
  290. open++;
  291. break;
  292. case STATUS_REOPENED:
  293. reopened++;
  294. break;
  295. case STATUS_CONFIRMED:
  296. confirmed++;
  297. break;
  298. default:
  299. // Other statuses are ignored
  300. }
  301. addIssueToImpactsBag(issue);
  302. }
  303. private void addIssueToImpactsBag(DefaultIssue issue) {
  304. if (IssueStatus.OPEN == issue.issueStatus() || IssueStatus.CONFIRMED == issue.issueStatus()) {
  305. for (Map.Entry<SoftwareQuality, Severity> impact : issue.impacts().entrySet()) {
  306. impactsBag.compute(impact.getKey().name(), (key, value) -> {
  307. value.compute(impact.getValue().name(), (severity, count) -> count == null ? 1 : count + 1);
  308. value.compute(ImpactMeasureBuilder.TOTAL_KEY, (total, count) -> count == null ? 1 : count + 1);
  309. return value;
  310. });
  311. }
  312. }
  313. }
  314. }
  315. /**
  316. * List of {@link Counter} for regular value and period.
  317. */
  318. private static class Counters {
  319. private final Counter counter = new Counter();
  320. private final Counter counterForPeriod = new Counter();
  321. void add(@Nullable Counters other) {
  322. if (other != null) {
  323. counter.add(other.counter);
  324. counterForPeriod.add(other.counterForPeriod);
  325. }
  326. }
  327. void addOnPeriod(DefaultIssue issue) {
  328. counterForPeriod.add(issue);
  329. }
  330. void add(DefaultIssue issue) {
  331. counter.add(issue);
  332. }
  333. Counter counter() {
  334. return counter;
  335. }
  336. Counter counterForPeriod() {
  337. return counterForPeriod;
  338. }
  339. }
  340. }