3 * Copyright (C) 2009-2021 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.ArrayList;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.List;
27 import java.util.Optional;
28 import javax.annotation.Nullable;
29 import org.sonar.api.rule.RuleKey;
30 import org.sonar.api.rules.RuleType;
31 import org.sonar.api.utils.Duration;
32 import org.sonar.api.utils.log.Loggers;
33 import org.sonar.ce.task.projectanalysis.batch.BatchReportReader;
34 import org.sonar.ce.task.projectanalysis.component.Component;
35 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
36 import org.sonar.ce.task.projectanalysis.issue.commonrule.CommonRuleEngine;
37 import org.sonar.ce.task.projectanalysis.issue.filter.IssueFilter;
38 import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder;
39 import org.sonar.ce.task.projectanalysis.source.SourceLinesHashRepository;
40 import org.sonar.core.issue.DefaultIssue;
41 import org.sonar.core.issue.tracking.Input;
42 import org.sonar.core.issue.tracking.LazyInput;
43 import org.sonar.core.issue.tracking.LineHashSequence;
44 import org.sonar.core.util.CloseableIterator;
45 import org.sonar.db.protobuf.DbCommons;
46 import org.sonar.db.protobuf.DbIssues;
47 import org.sonar.scanner.protocol.Constants.Severity;
48 import org.sonar.scanner.protocol.output.ScannerReport;
49 import org.sonar.scanner.protocol.output.ScannerReport.IssueType;
50 import org.sonar.server.rule.CommonRuleKeys;
52 import static org.apache.commons.lang.StringUtils.isNotEmpty;
53 import static org.sonar.api.issue.Issue.STATUS_OPEN;
54 import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
56 public class TrackerRawInputFactory {
57 private static final long DEFAULT_EXTERNAL_ISSUE_EFFORT = 0L;
58 private final TreeRootHolder treeRootHolder;
59 private final BatchReportReader reportReader;
60 private final CommonRuleEngine commonRuleEngine;
61 private final IssueFilter issueFilter;
62 private final SourceLinesHashRepository sourceLinesHash;
63 private final RuleRepository ruleRepository;
64 private final ActiveRulesHolder activeRulesHolder;
66 public TrackerRawInputFactory(TreeRootHolder treeRootHolder, BatchReportReader reportReader,
67 SourceLinesHashRepository sourceLinesHash, CommonRuleEngine commonRuleEngine, IssueFilter issueFilter, RuleRepository ruleRepository,
68 ActiveRulesHolder activeRulesHolder) {
69 this.treeRootHolder = treeRootHolder;
70 this.reportReader = reportReader;
71 this.sourceLinesHash = sourceLinesHash;
72 this.commonRuleEngine = commonRuleEngine;
73 this.issueFilter = issueFilter;
74 this.ruleRepository = ruleRepository;
75 this.activeRulesHolder = activeRulesHolder;
78 public Input<DefaultIssue> create(Component component) {
79 return new RawLazyInput(component);
82 private class RawLazyInput extends LazyInput<DefaultIssue> {
83 private final Component component;
85 private RawLazyInput(Component component) {
86 this.component = component;
90 protected LineHashSequence loadLineHashSequence() {
91 if (component.getType() == Component.Type.FILE) {
92 return new LineHashSequence(sourceLinesHash.getLineHashesMatchingDBVersion(component));
94 return new LineHashSequence(Collections.emptyList());
99 protected List<DefaultIssue> loadIssues() {
100 List<DefaultIssue> result = new ArrayList<>();
102 for (DefaultIssue commonRuleIssue : commonRuleEngine.process(component)) {
103 if (issueFilter.accept(commonRuleIssue, component)) {
104 result.add(init(commonRuleIssue, STATUS_OPEN));
108 if (component.getReportAttributes().getRef() == null) {
112 try (CloseableIterator<ScannerReport.Issue> reportIssues = reportReader.readComponentIssues(component.getReportAttributes().getRef())) {
113 // optimization - do not load line hashes if there are no issues -> getLineHashSequence() is executed
114 // as late as possible
115 while (reportIssues.hasNext()) {
116 ScannerReport.Issue reportIssue = reportIssues.next();
117 if (isOnInactiveRule(reportIssue)) {
120 if (!isIssueOnUnsupportedCommonRule(reportIssue)) {
121 Loggers.get(getClass()).debug("Ignored issue from analysis report on rule {}:{}", reportIssue.getRuleRepository(), reportIssue.getRuleKey());
124 DefaultIssue issue = toIssue(getLineHashSequence(), reportIssue);
125 if (issueFilter.accept(issue, component)) {
131 Map<RuleKey, ScannerReport.AdHocRule> adHocRuleMap = new HashMap<>();
132 try (CloseableIterator<ScannerReport.AdHocRule> reportAdHocRule = reportReader.readAdHocRules()) {
133 while (reportAdHocRule.hasNext()) {
134 ScannerReport.AdHocRule adHocRule = reportAdHocRule.next();
135 adHocRuleMap.put(RuleKey.of(RuleKey.EXTERNAL_RULE_REPO_PREFIX + adHocRule.getEngineId(), adHocRule.getRuleId()), adHocRule);
139 try (CloseableIterator<ScannerReport.ExternalIssue> reportExternalIssues = reportReader.readComponentExternalIssues(component.getReportAttributes().getRef())) {
140 // optimization - do not load line hashes if there are no issues -> getLineHashSequence() is executed
141 // as late as possible
142 while (reportExternalIssues.hasNext()) {
143 ScannerReport.ExternalIssue reportExternalIssue = reportExternalIssues.next();
144 result.add(toExternalIssue(getLineHashSequence(), reportExternalIssue, adHocRuleMap));
151 private boolean isOnInactiveRule(ScannerReport.Issue reportIssue) {
152 RuleKey ruleKey = RuleKey.of(reportIssue.getRuleRepository(), reportIssue.getRuleKey());
153 return !activeRulesHolder.get(ruleKey).isPresent();
156 private boolean isIssueOnUnsupportedCommonRule(ScannerReport.Issue issue) {
157 // issues on batch common rules are ignored. This feature
158 // is natively supported by compute engine since 5.2.
159 return !issue.getRuleRepository().startsWith(CommonRuleKeys.REPOSITORY_PREFIX);
162 private DefaultIssue toIssue(LineHashSequence lineHashSeq, ScannerReport.Issue reportIssue) {
163 DefaultIssue issue = new DefaultIssue();
164 init(issue, STATUS_OPEN);
165 RuleKey ruleKey = RuleKey.of(reportIssue.getRuleRepository(), reportIssue.getRuleKey());
166 issue.setRuleKey(ruleKey);
167 if (reportIssue.hasTextRange()) {
168 int startLine = reportIssue.getTextRange().getStartLine();
169 issue.setLine(startLine);
170 issue.setChecksum(lineHashSeq.getHashForLine(startLine));
172 issue.setChecksum("");
174 if (isNotEmpty(reportIssue.getMsg())) {
175 issue.setMessage(reportIssue.getMsg());
177 Rule rule = ruleRepository.getByKey(ruleKey);
178 issue.setMessage(rule.getName());
180 if (reportIssue.getSeverity() != Severity.UNSET_SEVERITY) {
181 issue.setSeverity(reportIssue.getSeverity().name());
183 if (Double.compare(reportIssue.getGap(), 0D) != 0) {
184 issue.setGap(reportIssue.getGap());
186 DbIssues.Locations.Builder dbLocationsBuilder = DbIssues.Locations.newBuilder();
187 if (reportIssue.hasTextRange()) {
188 dbLocationsBuilder.setTextRange(convertTextRange(reportIssue.getTextRange()));
190 for (ScannerReport.Flow flow : reportIssue.getFlowList()) {
191 if (flow.getLocationCount() > 0) {
192 DbIssues.Flow.Builder dbFlowBuilder = DbIssues.Flow.newBuilder();
193 for (ScannerReport.IssueLocation location : flow.getLocationList()) {
194 convertLocation(location).ifPresent(dbFlowBuilder::addLocation);
196 dbLocationsBuilder.addFlow(dbFlowBuilder);
199 issue.setIsFromExternalRuleEngine(false);
200 issue.setLocations(dbLocationsBuilder.build());
204 private DefaultIssue toExternalIssue(LineHashSequence lineHashSeq, ScannerReport.ExternalIssue reportExternalIssue, Map<RuleKey, ScannerReport.AdHocRule> adHocRuleMap) {
205 DefaultIssue issue = new DefaultIssue();
206 RuleType type = toRuleType(reportExternalIssue.getType());
207 init(issue, type == RuleType.SECURITY_HOTSPOT ? STATUS_TO_REVIEW : STATUS_OPEN);
209 RuleKey ruleKey = RuleKey.of(RuleKey.EXTERNAL_RULE_REPO_PREFIX + reportExternalIssue.getEngineId(), reportExternalIssue.getRuleId());
210 issue.setRuleKey(ruleKey);
211 if (reportExternalIssue.hasTextRange()) {
212 int startLine = reportExternalIssue.getTextRange().getStartLine();
213 issue.setLine(startLine);
214 issue.setChecksum(lineHashSeq.getHashForLine(startLine));
216 issue.setChecksum("");
218 if (isNotEmpty(reportExternalIssue.getMsg())) {
219 issue.setMessage(reportExternalIssue.getMsg());
221 if (reportExternalIssue.getSeverity() != Severity.UNSET_SEVERITY) {
222 issue.setSeverity(reportExternalIssue.getSeverity().name());
224 issue.setEffort(Duration.create(reportExternalIssue.getEffort() != 0 ? reportExternalIssue.getEffort() : DEFAULT_EXTERNAL_ISSUE_EFFORT));
225 DbIssues.Locations.Builder dbLocationsBuilder = DbIssues.Locations.newBuilder();
226 if (reportExternalIssue.hasTextRange()) {
227 dbLocationsBuilder.setTextRange(convertTextRange(reportExternalIssue.getTextRange()));
229 for (ScannerReport.Flow flow : reportExternalIssue.getFlowList()) {
230 if (flow.getLocationCount() > 0) {
231 DbIssues.Flow.Builder dbFlowBuilder = DbIssues.Flow.newBuilder();
232 for (ScannerReport.IssueLocation location : flow.getLocationList()) {
233 convertLocation(location).ifPresent(dbFlowBuilder::addLocation);
235 dbLocationsBuilder.addFlow(dbFlowBuilder);
238 issue.setIsFromExternalRuleEngine(true);
239 issue.setLocations(dbLocationsBuilder.build());
242 ruleRepository.addOrUpdateAddHocRuleIfNeeded(ruleKey, () -> toAdHocRule(reportExternalIssue, adHocRuleMap.get(issue.ruleKey())));
246 private NewAdHocRule toAdHocRule(ScannerReport.ExternalIssue reportIssue, @Nullable ScannerReport.AdHocRule adHocRule) {
247 if (adHocRule != null) {
248 return new NewAdHocRule(adHocRule);
250 return new NewAdHocRule(reportIssue);
253 private RuleType toRuleType(IssueType type) {
258 return RuleType.CODE_SMELL;
260 return RuleType.VULNERABILITY;
261 case SECURITY_HOTSPOT:
262 return RuleType.SECURITY_HOTSPOT;
265 throw new IllegalStateException("Invalid issue type: " + type);
269 private DefaultIssue init(DefaultIssue issue, String initialStatus) {
270 issue.setStatus(initialStatus);
271 issue.setResolution(null);
272 issue.setComponentUuid(component.getUuid());
273 issue.setComponentKey(component.getKey());
274 issue.setProjectUuid(treeRootHolder.getRoot().getUuid());
275 issue.setProjectKey(treeRootHolder.getRoot().getKey());
279 private Optional<DbIssues.Location> convertLocation(ScannerReport.IssueLocation source) {
280 DbIssues.Location.Builder target = DbIssues.Location.newBuilder();
281 if (source.getComponentRef() != 0 && source.getComponentRef() != component.getReportAttributes().getRef()) {
282 // SONAR-10781 Component might not exist because on PR, only changed components are included in the report
283 Optional<Component> optionalComponent = treeRootHolder.getOptionalComponentByRef(source.getComponentRef());
284 if (!optionalComponent.isPresent()) {
285 return Optional.empty();
287 target.setComponentId(optionalComponent.get().getUuid());
289 if (isNotEmpty(source.getMsg())) {
290 target.setMsg(source.getMsg());
292 if (source.hasTextRange()) {
293 ScannerReport.TextRange sourceRange = source.getTextRange();
294 DbCommons.TextRange.Builder targetRange = convertTextRange(sourceRange);
295 target.setTextRange(targetRange);
297 return Optional.of(target.build());
300 private DbCommons.TextRange.Builder convertTextRange(ScannerReport.TextRange sourceRange) {
301 DbCommons.TextRange.Builder targetRange = DbCommons.TextRange.newBuilder();
302 targetRange.setStartLine(sourceRange.getStartLine());
303 targetRange.setStartOffset(sourceRange.getStartOffset());
304 targetRange.setEndLine(sourceRange.getEndLine());
305 targetRange.setEndOffset(sourceRange.getEndOffset());