diff --git a/cxx-sensors/src/main/java/org/sonar/cxx/sensors/pclint/CxxPCLintSensor.java b/cxx-sensors/src/main/java/org/sonar/cxx/sensors/pclint/CxxPCLintSensor.java index 7e2a05adf2..d015c60459 100644 --- a/cxx-sensors/src/main/java/org/sonar/cxx/sensors/pclint/CxxPCLintSensor.java +++ b/cxx-sensors/src/main/java/org/sonar/cxx/sensors/pclint/CxxPCLintSensor.java @@ -1,171 +1,239 @@ -/* - * Sonar C++ Plugin (Community) - * Copyright (C) 2010-2019 SonarOpenCommunity - * http://github.com/SonarOpenCommunity/sonar-cxx - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.cxx.sensors.pclint; - -import java.io.File; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nullable; -import javax.xml.stream.XMLStreamException; -import org.codehaus.staxmate.in.SMHierarchicCursor; -import org.codehaus.staxmate.in.SMInputCursor; -import org.sonar.api.batch.sensor.SensorContext; -import org.sonar.api.batch.sensor.SensorDescriptor; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; -import org.sonar.cxx.CxxLanguage; -import org.sonar.cxx.CxxMetricsFactory; -import org.sonar.cxx.sensors.utils.CxxIssuesReportSensor; -import org.sonar.cxx.sensors.utils.CxxUtils; -import org.sonar.cxx.sensors.utils.EmptyReportException; -import org.sonar.cxx.sensors.utils.StaxParser; -import org.sonar.cxx.utils.CxxReportIssue; - -/** - * PC-lint is an equivalent to pmd but for C++ The first version of the tool was release 1985 and the tool analyzes - * C/C++ source code from many compiler vendors. PC-lint is the version for Windows and FlexLint for Unix, VMS, OS-9, - * etc See also: http://www.gimpel.com/html/index.htm - * - * @author Bert - */ -public class CxxPCLintSensor extends CxxIssuesReportSensor { - - public static final String REPORT_PATH_KEY = "pclint.reportPath"; - public static final Pattern MISRA_RULE_PATTERN = Pattern.compile( - // Rule nn.nn -or- Rule nn-nn-nn - "Rule\\x20(\\d{1,2}.\\d{1,2}|\\d{1,2}-\\d{1,2}-\\d{1,2})(,|\\])"); - private static final Logger LOG = Loggers.get(CxxPCLintSensor.class); - - /** - * CxxPCLintSensor for PC-lint Sensor - * - * @param language defines settings C or C++ - */ - public CxxPCLintSensor(CxxLanguage language) { - super(language, REPORT_PATH_KEY, CxxPCLintRuleRepository.getRepositoryKey(language)); - } - - @Override - public void describe(SensorDescriptor descriptor) { - descriptor - .name(getLanguage().getName() + " PCLintSensor") - .onlyOnLanguage(getLanguage().getKey()) - .createIssuesForRuleRepository(getRuleRepositoryKey()) - .onlyWhenConfiguration(conf -> conf.hasKey(getReportPathKey())); - } - - @Override - protected void processReport(final SensorContext context, File report) - throws javax.xml.stream.XMLStreamException { - LOG.debug("Parsing 'PC-Lint' format"); - - StaxParser parser = new StaxParser(new StaxParser.XmlStreamHandler() { - /** - * {@inheritDoc} - */ - @Override - public void stream(SMHierarchicCursor rootCursor) throws XMLStreamException { - try { - rootCursor.advance(); - } catch (com.ctc.wstx.exc.WstxEOFException eofExc) { - throw new EmptyReportException("Cannot read PClint report", eofExc); - } - - SMInputCursor errorCursor = rootCursor.childElementCursor("issue"); - try { - while (errorCursor.getNext() != null) { - String file = errorCursor.getAttrValue("file"); - String line = errorCursor.getAttrValue("line"); - String id = errorCursor.getAttrValue("number"); - String msg = errorCursor.getAttrValue("desc"); - - if (isInputValid(file, line, id, msg)) { - if (msg.contains("MISRA")) { - //remap MISRA IDs. Only Unique rules for MISRA C 2004 and MISRA C/C++ 2008 - // have been created in the rule repository - if (msg.contains("MISRA 2004") || msg.contains("MISRA 2008") - || msg.contains("MISRA C++ 2008") || msg.contains("MISRA C++ Rule")) { - id = mapMisraRulesToUniqueSonarRules(msg, Boolean.FALSE); - } else if (msg.contains("MISRA 2012 Rule")) { - id = mapMisraRulesToUniqueSonarRules(msg, Boolean.TRUE); - } - } - - CxxReportIssue issue = new CxxReportIssue(id, file, line, msg); - saveUniqueViolation(context, issue); - } else { - LOG.warn("PC-lint warning ignored: {}", msg); - if (LOG.isDebugEnabled()) { - LOG.debug("File: {}, Line: {}, ID: {}, msg: {}", file, line, id, msg); - } - } - } - } catch (com.ctc.wstx.exc.WstxUnexpectedCharException - | com.ctc.wstx.exc.WstxEOFException - | com.ctc.wstx.exc.WstxIOException e) { - LOG.error("Ignore XML error from PC-lint '{}'", CxxUtils.getStackTrace(e)); - } - } - - private boolean isInputValid(@Nullable String file, @Nullable String line, - @Nullable String id, @Nullable String msg) { - try { - if (file == null || file.isEmpty() || (Integer.parseInt(line) == 0)) { - // issue for project or file level - return id != null && !id.isEmpty() && msg != null && !msg.isEmpty(); - } - return !file.isEmpty() && id != null && !id.isEmpty() && msg != null && !msg.isEmpty(); - } catch (java.lang.NumberFormatException e) { - LOG.error("Ignore number error from PC-lint report '{}'", CxxUtils.getStackTrace(e)); - } - return false; - } - - /** - * Concatenate M with the MISRA rule number to get the new rule id to save the violation to. - */ - private String mapMisraRulesToUniqueSonarRules(String msg, Boolean isMisra2012) { - Matcher matcher = MISRA_RULE_PATTERN.matcher(msg); - if (matcher.find()) { - String misraRule = matcher.group(1); - String newKey; - if (isMisra2012) { - newKey = "M2012-" + misraRule; - } else { - newKey = "M" + misraRule; - } - if (LOG.isDebugEnabled()) { - LOG.debug("Remap MISRA rule {} to key {}", misraRule, newKey); - } - return newKey; - } - return ""; - } - }); - - parser.parse(report); - } - - @Override - protected CxxMetricsFactory.Key getMetricKey() { - return CxxMetricsFactory.Key.PCLINT_SENSOR_ISSUES_KEY; - } - -} +/* + * Sonar C++ Plugin (Community) + * Copyright (C) 2010-2019 SonarOpenCommunity + * http://github.com/SonarOpenCommunity/sonar-cxx + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.cxx.sensors.pclint; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.xml.stream.XMLStreamException; +import org.codehaus.staxmate.in.SMHierarchicCursor; +import org.codehaus.staxmate.in.SMInputCursor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.cxx.CxxLanguage; +import org.sonar.cxx.CxxMetricsFactory; +import org.sonar.cxx.sensors.utils.CxxIssuesReportSensor; +import org.sonar.cxx.sensors.utils.CxxUtils; +import org.sonar.cxx.sensors.utils.EmptyReportException; +import org.sonar.cxx.sensors.utils.StaxParser; +import org.sonar.cxx.utils.CxxReportIssue; +import org.sonar.cxx.utils.CxxReportLocation; + +/** + * PC-lint is an equivalent to pmd but for C++ The first version of the tool was release 1985 and the tool analyzes + * C/C++ source code from many compiler vendors. PC-lint is the version for Windows and FlexLint for Unix, VMS, OS-9, + * etc See also: http://www.gimpel.com/html/index.htm + * + * @author Bert + */ +public class CxxPCLintSensor extends CxxIssuesReportSensor { + + public static final String REPORT_PATH_KEY = "pclint.reportPath"; + public static final Pattern MISRA_RULE_PATTERN = Pattern.compile( + // Rule nn.nn -or- Rule nn-nn-nn + "Rule\\x20(\\d{1,2}.\\d{1,2}|\\d{1,2}-\\d{1,2}-\\d{1,2})(,|\\])"); + private static final Logger LOG = Loggers.get(CxxPCLintSensor.class); + + private static final String SUPPLEMENTAL_TYPE_ISSUE = "supplemental"; + + private static final String PREFIX_DURING_SPECIFIC_WALK_MSG = "during specific walk"; + + private static final Pattern SUPPLEMENTAL_MSG_PATTERN = + Pattern.compile(PREFIX_DURING_SPECIFIC_WALK_MSG + "\\s+(.+):(\\d+):(\\d+)\\s+.+"); + + /** + * CxxPCLintSensor for PC-lint Sensor + * + * @param language defines settings C or C++ + */ + public CxxPCLintSensor(CxxLanguage language) { + super(language, REPORT_PATH_KEY, CxxPCLintRuleRepository.getRepositoryKey(language)); + } + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor + .name(getLanguage().getName() + " PCLintSensor") + .onlyOnLanguage(getLanguage().getKey()) + .createIssuesForRuleRepository(getRuleRepositoryKey()) + .onlyWhenConfiguration(conf -> conf.hasKey(getReportPathKey())); + } + + @Override + protected void processReport(final SensorContext context, File report) + throws javax.xml.stream.XMLStreamException { + LOG.debug("Parsing 'PC-Lint' format"); + + StaxParser parser = new StaxParser(new StaxParser.XmlStreamHandler() { + /** + * {@inheritDoc} + */ + @Override + public void stream(SMHierarchicCursor rootCursor) throws XMLStreamException { + try { + rootCursor.advance(); + } catch (com.ctc.wstx.exc.WstxEOFException eofExc) { + throw new EmptyReportException("Cannot read PClint report", eofExc); + } + + // collect the "supplemental" messages generated by pc-lint, + // which are very helpful to track why/when the original issue is found. + // The "supplemental" messages will be right after the original issue. + // NOTE: Require the "type" attribute in the report. + CxxReportIssue currentIssue = null; + + SMInputCursor errorCursor = rootCursor.childElementCursor("issue"); + try { + while (errorCursor.getNext() != null) { + String file = errorCursor.getAttrValue("file"); + String line = errorCursor.getAttrValue("line"); + String id = errorCursor.getAttrValue("number"); + String msg = errorCursor.getAttrValue("desc"); + String type = errorCursor.getAttrValue("type"); + + // handle the case when supplemental message has no file and line + // eg, issue 894. + if (SUPPLEMENTAL_TYPE_ISSUE.equals(type) && currentIssue != null) { + addSecondaryLocationsToCurrentIssue(currentIssue, file, line, msg); + continue; + } + + if (isInputValid(file, line, id, msg)) { + if (msg.contains("MISRA")) { + //remap MISRA IDs. Only Unique rules for MISRA C 2004 and MISRA C/C++ 2008 + // have been created in the rule repository + if (msg.contains("MISRA 2004") || msg.contains("MISRA 2008") + || msg.contains("MISRA C++ 2008") || msg.contains("MISRA C++ Rule")) { + id = mapMisraRulesToUniqueSonarRules(msg, Boolean.FALSE); + } else if (msg.contains("MISRA 2012 Rule")) { + id = mapMisraRulesToUniqueSonarRules(msg, Boolean.TRUE); + } + } + + if (currentIssue != null) { + saveUniqueViolation(context, currentIssue); + } + + currentIssue = new CxxReportIssue(id, file, line, msg); + } else { + LOG.warn("PC-lint warning ignored: {}", msg); + if (LOG.isDebugEnabled()) { + LOG.debug("File: {}, Line: {}, ID: {}, msg: {}", file, line, id, msg); + } + } + } + + if (currentIssue != null) { + saveUniqueViolation(context, currentIssue); + } + } catch (com.ctc.wstx.exc.WstxUnexpectedCharException + | com.ctc.wstx.exc.WstxEOFException + | com.ctc.wstx.exc.WstxIOException e) { + LOG.error("Ignore XML error from PC-lint '{}'", CxxUtils.getStackTrace(e)); + } + } + + private void addSecondaryLocationsToCurrentIssue(@Nonnull CxxReportIssue currentIssue, + String file, + String line, + String msg) { + if (currentIssue.getLocations().isEmpty()) { + LOG.error("The issue of {} must have the primary location. Skip adding more locations", + currentIssue.toString()); + return; + } + + if (file != null && file.isEmpty() && msg != null) { + Matcher matcher = SUPPLEMENTAL_MSG_PATTERN.matcher(msg); + + if (matcher.matches()) { + file = matcher.group(1); + line = matcher.group(2); + } + } + + if (file == null || file.isEmpty() || line == null || line.isEmpty()) { + return; + } + + // Due to SONAR-9929, even the API supports the extra/flow in different file, + // the UI is not ready. For this case, use the parent issue's file and line for now. + CxxReportLocation primaryLocation = currentIssue.getLocations().get(0); + if (!primaryLocation.getFile().equals(file)) { + if (!msg.startsWith(PREFIX_DURING_SPECIFIC_WALK_MSG)) { + msg = String.format("%s %s:%s %s", PREFIX_DURING_SPECIFIC_WALK_MSG, file, line, msg); + } + + file = primaryLocation.getFile(); + line = primaryLocation.getLine(); + } + + currentIssue.addFlowElement(file, line, msg); + } + + private boolean isInputValid(@Nullable String file, @Nullable String line, + @Nullable String id, @Nullable String msg) { + try { + if (file == null || file.isEmpty() || (Integer.parseInt(line) == 0)) { + // issue for project or file level + return id != null && !id.isEmpty() && msg != null && !msg.isEmpty(); + } + return !file.isEmpty() && id != null && !id.isEmpty() && msg != null && !msg.isEmpty(); + } catch (java.lang.NumberFormatException e) { + LOG.error("Ignore number error from PC-lint report '{}'", CxxUtils.getStackTrace(e)); + } + return false; + } + + /** + * Concatenate M with the MISRA rule number to get the new rule id to save the violation to. + */ + private String mapMisraRulesToUniqueSonarRules(String msg, Boolean isMisra2012) { + Matcher matcher = MISRA_RULE_PATTERN.matcher(msg); + if (matcher.find()) { + String misraRule = matcher.group(1); + String newKey; + if (isMisra2012) { + newKey = "M2012-" + misraRule; + } else { + newKey = "M" + misraRule; + } + if (LOG.isDebugEnabled()) { + LOG.debug("Remap MISRA rule {} to key {}", misraRule, newKey); + } + return newKey; + } + return ""; + } + }); + + parser.parse(report); + } + + @Override + protected CxxMetricsFactory.Key getMetricKey() { + return CxxMetricsFactory.Key.PCLINT_SENSOR_ISSUES_KEY; + } + +} diff --git a/cxx-sensors/src/main/java/org/sonar/cxx/sensors/utils/CxxIssuesReportSensor.java b/cxx-sensors/src/main/java/org/sonar/cxx/sensors/utils/CxxIssuesReportSensor.java index d52f0fad0a..d9327a71a4 100644 --- a/cxx-sensors/src/main/java/org/sonar/cxx/sensors/utils/CxxIssuesReportSensor.java +++ b/cxx-sensors/src/main/java/org/sonar/cxx/sensors/utils/CxxIssuesReportSensor.java @@ -1,254 +1,254 @@ -/* - * Sonar C++ Plugin (Community) - * Copyright (C) 2010-2019 SonarOpenCommunity - * http://github.com/SonarOpenCommunity/sonar-cxx - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.cxx.sensors.utils; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nullable; -import org.sonar.api.batch.fs.InputFile; -import org.sonar.api.batch.sensor.SensorContext; -import org.sonar.api.batch.sensor.issue.NewIssue; -import org.sonar.api.batch.sensor.issue.NewIssueLocation; -import org.sonar.api.measures.Metric; -import org.sonar.api.rule.RuleKey; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; -import org.sonar.cxx.CxxLanguage; -import org.sonar.cxx.CxxMetricsFactory; -import org.sonar.cxx.utils.CxxReportIssue; -import org.sonar.cxx.utils.CxxReportLocation; - -/** - * This class is used as base for all sensors which import external reports, which contain issues. It hosts common logic - * such as saving issues in SonarQube - */ -public abstract class CxxIssuesReportSensor extends CxxReportSensor { - - private static final Logger LOG = Loggers.get(CxxIssuesReportSensor.class); - - private final Set uniqueIssues = new HashSet<>(); - private final Map violationsPerFileCount = new HashMap<>(); - private int violationsPerModuleCount; - private final String ruleRepositoryKey; - - /** - * {@inheritDoc} - */ - protected CxxIssuesReportSensor(CxxLanguage language, String propertiesKeyPathToReports, String ruleRepositoryKey) { - super(language, propertiesKeyPathToReports); - this.ruleRepositoryKey = ruleRepositoryKey; - } - - private static NewIssueLocation createNewIssueLocationModule(SensorContext sensorContext, NewIssue newIssue, - CxxReportLocation location) { - return newIssue.newLocation().on(sensorContext.module()).message(location.getInfo()); - } - - public String getRuleRepositoryKey() { - return ruleRepositoryKey; - } - - /** - * {@inheritDoc} - */ - @Override - public void executeImpl(SensorContext context) { - try { - LOG.info("Searching reports by relative path with basedir '{}' and search prop '{}'", - context.fileSystem().baseDir(), getReportPathKey()); - List reports = getReports(context.config(), context.fileSystem().baseDir(), getReportPathKey()); - violationsPerFileCount.clear(); - violationsPerModuleCount = 0; - - for (File report : reports) { - int prevViolationsCount = violationsPerModuleCount; - LOG.info("Processing report '{}'", report); - executeReport(context, report, prevViolationsCount); - } - - Metric metric = getLanguage().getMetric(this.getMetricKey()); - LOG.info("{} processed = {}", metric.getKey(), violationsPerModuleCount); - - for (Map.Entry entry : violationsPerFileCount.entrySet()) { - context.newMeasure() - .forMetric(metric) - .on(entry.getKey()) - .withValue(entry.getValue()) - .save(); - } - - // this sensor could be executed on module without any files - // (possible for hierarchical multi-module projects) - // don't publish 0 as module metric, - // let AggregateMeasureComputer calculate the correct value - if (violationsPerModuleCount != 0) { - context.newMeasure() - .forMetric(metric) - .on(context.module()) - .withValue(violationsPerModuleCount) - .save(); - } - } catch (Exception e) { - String msg = new StringBuilder(256) - .append("Cannot feed the data into sonar, details: '") - .append(CxxUtils.getStackTrace(e)) - .append("'") - .toString(); - LOG.error(msg); - CxxUtils.validateRecovery(e, getLanguage()); - } - } - - /** - * Saves code violation only if it wasn't already saved - * - * @param sensorContext - * @param issue - */ - public void saveUniqueViolation(SensorContext sensorContext, CxxReportIssue issue) { - if (uniqueIssues.add(issue)) { - saveViolation(sensorContext, issue); - } - } - - /** - * @param context - * @param report - * @param prevViolationsCount - * @throws Exception - */ - private void executeReport(SensorContext context, File report, int prevViolationsCount) throws Exception { - try { - processReport(context, report); - if (LOG.isDebugEnabled()) { - Metric metric = getLanguage().getMetric(this.getMetricKey()); - LOG.debug("{} processed = {}", metric.getKey(), - violationsPerModuleCount - prevViolationsCount); - } - } catch (EmptyReportException e) { - LOG.warn("The report '{}' seems to be empty, ignoring.", report); - LOG.debug("Cannot read report", e); - CxxUtils.validateRecovery(e, getLanguage()); - } - } - - private NewIssueLocation createNewIssueLocationFile(SensorContext sensorContext, NewIssue newIssue, - CxxReportLocation location, Set affectedFiles) { - InputFile inputFile = getInputFileIfInProject(sensorContext, location.getFile()); - if (inputFile != null) { - int lines = inputFile.lines(); - int lineNr = Integer.max(1, getLineAsInt(location.getLine(), lines)); - NewIssueLocation newIssueLocation = newIssue.newLocation().on(inputFile).at(inputFile.selectLine(lineNr)) - .message(location.getInfo()); - affectedFiles.add(inputFile); - return newIssueLocation; - } - return null; - } - - /** - * Saves a code violation which is detected in the given file/line and has given ruleId and message. Saves it to the - * given project and context. Project or file-level violations can be saved by passing null for the according - * parameters ('file' = null for project level, 'line' = null for file-level) - */ - private void saveViolation(SensorContext sensorContext, CxxReportIssue issue) { - NewIssue newIssue = sensorContext.newIssue().forRule(RuleKey.of(getRuleRepositoryKey(), issue.getRuleId())); - - Set affectedFiles = new HashSet<>(); - List newIssueLocations = new ArrayList<>(); - List newIssueFlow = new ArrayList<>(); - - for (CxxReportLocation location : issue.getLocations()) { - if (location.getFile() != null && !location.getFile().isEmpty()) { - NewIssueLocation newIssueLocation = createNewIssueLocationFile(sensorContext, newIssue, location, - affectedFiles); - if (newIssueLocation != null) { - newIssueLocations.add(newIssueLocation); - } - } else { - NewIssueLocation newIssueLocation = createNewIssueLocationModule(sensorContext, newIssue, location); - newIssueLocations.add(newIssueLocation); - } - } - - for (CxxReportLocation location : issue.getFlow()) { - NewIssueLocation newIssueLocation = createNewIssueLocationFile(sensorContext, newIssue, location, affectedFiles); - if (newIssueLocation != null) { - newIssueFlow.add(newIssueLocation); - } else { - LOG.debug("Failed to create new issue location from flow location {}", location); - newIssueFlow.clear(); - break; - } - } - - if (!newIssueLocations.isEmpty()) { - try { - newIssue.at(newIssueLocations.get(0)); - for (int i = 1; i < newIssueLocations.size(); i++) { - newIssue.addLocation(newIssueLocations.get(i)); - } - // paths with just one element are not reported as flows to avoid - // presenting 1-element flows in SonarQube UI - if (newIssueFlow.size() > 1) { - newIssue.addFlow(newIssueFlow); - } - newIssue.save(); - - for (InputFile affectedFile : affectedFiles) { - violationsPerFileCount.merge(affectedFile, 1, Integer::sum); - } - violationsPerModuleCount++; - } catch (RuntimeException ex) { - LOG.error("Could not add the issue '{}':{}', skipping issue", issue.toString(), CxxUtils.getStackTrace(ex)); - CxxUtils.validateRecovery(ex, getLanguage()); - } - } - } - - private int getLineAsInt(@Nullable String line, int maxLine) { - int lineNr = 0; - if (line != null) { - try { - lineNr = Integer.parseInt(line); - if (lineNr < 1) { - lineNr = 1; - } else if (lineNr > maxLine) { // https://jira.sonarsource.com/browse/SONAR-6792 - lineNr = maxLine; - } - } catch (java.lang.NumberFormatException nfe) { - LOG.warn("Skipping invalid line number: {}", line); - CxxUtils.validateRecovery(nfe, getLanguage()); - lineNr = -1; - } - } - return lineNr; - } - - protected abstract void processReport(final SensorContext context, File report) throws Exception; - - protected abstract CxxMetricsFactory.Key getMetricKey(); - -} +/* + * Sonar C++ Plugin (Community) + * Copyright (C) 2010-2019 SonarOpenCommunity + * http://github.com/SonarOpenCommunity/sonar-cxx + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.cxx.sensors.utils; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.measures.Metric; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.cxx.CxxLanguage; +import org.sonar.cxx.CxxMetricsFactory; +import org.sonar.cxx.utils.CxxReportIssue; +import org.sonar.cxx.utils.CxxReportLocation; + +/** + * This class is used as base for all sensors which import external reports, which contain issues. It hosts common logic + * such as saving issues in SonarQube + */ +public abstract class CxxIssuesReportSensor extends CxxReportSensor { + + private static final Logger LOG = Loggers.get(CxxIssuesReportSensor.class); + + private final Set uniqueIssues = new HashSet<>(); + private final Map violationsPerFileCount = new HashMap<>(); + private int violationsPerModuleCount; + private final String ruleRepositoryKey; + + /** + * {@inheritDoc} + */ + protected CxxIssuesReportSensor(CxxLanguage language, String propertiesKeyPathToReports, String ruleRepositoryKey) { + super(language, propertiesKeyPathToReports); + this.ruleRepositoryKey = ruleRepositoryKey; + } + + private static NewIssueLocation createNewIssueLocationModule(SensorContext sensorContext, NewIssue newIssue, + CxxReportLocation location) { + return newIssue.newLocation().on(sensorContext.module()).message(location.getInfo()); + } + + public String getRuleRepositoryKey() { + return ruleRepositoryKey; + } + + /** + * {@inheritDoc} + */ + @Override + public void executeImpl(SensorContext context) { + try { + LOG.info("Searching reports by relative path with basedir '{}' and search prop '{}'", + context.fileSystem().baseDir(), getReportPathKey()); + List reports = getReports(context.config(), context.fileSystem().baseDir(), getReportPathKey()); + violationsPerFileCount.clear(); + violationsPerModuleCount = 0; + + for (File report : reports) { + int prevViolationsCount = violationsPerModuleCount; + LOG.info("Processing report '{}'", report); + executeReport(context, report, prevViolationsCount); + } + + Metric metric = getLanguage().getMetric(this.getMetricKey()); + LOG.info("{} processed = {}", metric.getKey(), violationsPerModuleCount); + + for (Map.Entry entry : violationsPerFileCount.entrySet()) { + context.newMeasure() + .forMetric(metric) + .on(entry.getKey()) + .withValue(entry.getValue()) + .save(); + } + + // this sensor could be executed on module without any files + // (possible for hierarchical multi-module projects) + // don't publish 0 as module metric, + // let AggregateMeasureComputer calculate the correct value + if (violationsPerModuleCount != 0) { + context.newMeasure() + .forMetric(metric) + .on(context.module()) + .withValue(violationsPerModuleCount) + .save(); + } + } catch (Exception e) { + String msg = new StringBuilder(256) + .append("Cannot feed the data into sonar, details: '") + .append(CxxUtils.getStackTrace(e)) + .append("'") + .toString(); + LOG.error(msg); + CxxUtils.validateRecovery(e, getLanguage()); + } + } + + /** + * Saves code violation only if it wasn't already saved + * + * @param sensorContext + * @param issue + */ + public void saveUniqueViolation(SensorContext sensorContext, CxxReportIssue issue) { + if (uniqueIssues.add(issue)) { + saveViolation(sensorContext, issue); + } + } + + /** + * @param context + * @param report + * @param prevViolationsCount + * @throws Exception + */ + private void executeReport(SensorContext context, File report, int prevViolationsCount) throws Exception { + try { + processReport(context, report); + if (LOG.isDebugEnabled()) { + Metric metric = getLanguage().getMetric(this.getMetricKey()); + LOG.debug("{} processed = {}", metric.getKey(), + violationsPerModuleCount - prevViolationsCount); + } + } catch (EmptyReportException e) { + LOG.warn("The report '{}' seems to be empty, ignoring.", report); + LOG.debug("Cannot read report", e); + CxxUtils.validateRecovery(e, getLanguage()); + } + } + + private NewIssueLocation createNewIssueLocationFile(SensorContext sensorContext, NewIssue newIssue, + CxxReportLocation location, Set affectedFiles) { + InputFile inputFile = getInputFileIfInProject(sensorContext, location.getFile()); + if (inputFile != null) { + int lines = inputFile.lines(); + int lineNr = Integer.max(1, getLineAsInt(location.getLine(), lines)); + NewIssueLocation newIssueLocation = newIssue.newLocation().on(inputFile).at(inputFile.selectLine(lineNr)) + .message(location.getInfo()); + affectedFiles.add(inputFile); + return newIssueLocation; + } + return null; + } + + /** + * Saves a code violation which is detected in the given file/line and has given ruleId and message. Saves it to the + * given project and context. Project or file-level violations can be saved by passing null for the according + * parameters ('file' = null for project level, 'line' = null for file-level) + */ + private void saveViolation(SensorContext sensorContext, CxxReportIssue issue) { + NewIssue newIssue = sensorContext.newIssue().forRule(RuleKey.of(getRuleRepositoryKey(), issue.getRuleId())); + + Set affectedFiles = new HashSet<>(); + List newIssueLocations = new ArrayList<>(); + List newIssueFlow = new ArrayList<>(); + + for (CxxReportLocation location : issue.getLocations()) { + if (location.getFile() != null && !location.getFile().isEmpty()) { + NewIssueLocation newIssueLocation = createNewIssueLocationFile(sensorContext, newIssue, location, + affectedFiles); + if (newIssueLocation != null) { + newIssueLocations.add(newIssueLocation); + } + } else { + NewIssueLocation newIssueLocation = createNewIssueLocationModule(sensorContext, newIssue, location); + newIssueLocations.add(newIssueLocation); + } + } + + for (CxxReportLocation location : issue.getFlow()) { + NewIssueLocation newIssueLocation = createNewIssueLocationFile(sensorContext, newIssue, location, affectedFiles); + if (newIssueLocation != null) { + newIssueFlow.add(newIssueLocation); + } else { + LOG.debug("Failed to create new issue location from flow location {}", location); + newIssueFlow.clear(); + break; + } + } + + if (!newIssueLocations.isEmpty()) { + try { + newIssue.at(newIssueLocations.get(0)); + for (int i = 1; i < newIssueLocations.size(); i++) { + newIssue.addLocation(newIssueLocations.get(i)); + } + + if (!newIssueFlow.isEmpty()) { + newIssue.addFlow(newIssueFlow); + } + + newIssue.save(); + + for (InputFile affectedFile : affectedFiles) { + violationsPerFileCount.merge(affectedFile, 1, Integer::sum); + } + violationsPerModuleCount++; + } catch (RuntimeException ex) { + LOG.error("Could not add the issue '{}':{}', skipping issue", issue.toString(), CxxUtils.getStackTrace(ex)); + CxxUtils.validateRecovery(ex, getLanguage()); + } + } + } + + private int getLineAsInt(@Nullable String line, int maxLine) { + int lineNr = 0; + if (line != null) { + try { + lineNr = Integer.parseInt(line); + if (lineNr < 1) { + lineNr = 1; + } else if (lineNr > maxLine) { // https://jira.sonarsource.com/browse/SONAR-6792 + lineNr = maxLine; + } + } catch (java.lang.NumberFormatException nfe) { + LOG.warn("Skipping invalid line number: {}", line); + CxxUtils.validateRecovery(nfe, getLanguage()); + lineNr = -1; + } + } + return lineNr; + } + + protected abstract void processReport(final SensorContext context, File report) throws Exception; + + protected abstract CxxMetricsFactory.Key getMetricKey(); + +} diff --git a/cxx-sensors/src/test/java/org/sonar/cxx/sensors/clangsa/CxxClangSASensorTest.java b/cxx-sensors/src/test/java/org/sonar/cxx/sensors/clangsa/CxxClangSASensorTest.java index 9e971b39ab..0aaf4567b7 100644 --- a/cxx-sensors/src/test/java/org/sonar/cxx/sensors/clangsa/CxxClangSASensorTest.java +++ b/cxx-sensors/src/test/java/org/sonar/cxx/sensors/clangsa/CxxClangSASensorTest.java @@ -199,12 +199,12 @@ public void shouldReportCorrectFlows() { // presenting 1-element flows in SonarQube UI { Issue issue = Iterables.get(context.allIssues(), 1); - assertThat(issue.flows()).hasSize(0); + assertThat(issue.flows()).hasSize(1); } { Issue issue = Iterables.get(context.allIssues(), 2); - assertThat(issue.flows()).hasSize(0); + assertThat(issue.flows()).hasSize(1); } } diff --git a/cxx-sensors/src/test/java/org/sonar/cxx/sensors/pclint/CxxPCLintSensorTest.java b/cxx-sensors/src/test/java/org/sonar/cxx/sensors/pclint/CxxPCLintSensorTest.java index fba0d57649..a062557ad4 100644 --- a/cxx-sensors/src/test/java/org/sonar/cxx/sensors/pclint/CxxPCLintSensorTest.java +++ b/cxx-sensors/src/test/java/org/sonar/cxx/sensors/pclint/CxxPCLintSensorTest.java @@ -20,6 +20,7 @@ package org.sonar.cxx.sensors.pclint; import java.util.ArrayList; +import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import org.assertj.core.api.SoftAssertions; @@ -182,4 +183,29 @@ public void sensorDescriptor() { softly.assertAll(); } + @Test + public void loadSupplementalMsg() { + SensorContextTester context = SensorContextTester.create(fs.baseDir()); + + settings.setProperty(language.getPluginProperty(CxxPCLintSensor.REPORT_PATH_KEY), "pclint-reports/pclint-result-with-supplemental.xml"); + context.setSettings(settings); + + context.fileSystem().add(TestInputFileBuilder.create("ProjectKey", "FileZip.cpp").setLanguage("cpp").initMetadata("asd\nasdas\nasda\n").build()); + context.fileSystem().add(TestInputFileBuilder.create("ProjectKey", "FileZip.h").setLanguage("cpp").initMetadata("asd\nasdas\nasda\n").build()); + + CxxPCLintSensor sensor = new CxxPCLintSensor(language); + sensor.execute(context); + + assertThat(context.allIssues().size()).isEqualTo(2); + + List allIssues = new ArrayList<>(context.allIssues()); + + Issue firstIssue = allIssues.get(0); + assertThat(firstIssue.flows().size()).isEqualTo(1); + assertThat(firstIssue.flows().get(0).locations().size()).isEqualTo(3); + + Issue secondIssue = allIssues.get(1); + assertThat(secondIssue.flows().size()).isEqualTo(1); + assertThat(secondIssue.flows().get(0).locations().size()).isEqualTo(1); + } } diff --git a/cxx-sensors/src/test/resources/org/sonar/cxx/sensors/reports-project/pclint-reports/pclint-result-with-supplemental.xml b/cxx-sensors/src/test/resources/org/sonar/cxx/sensors/reports-project/pclint-reports/pclint-result-with-supplemental.xml new file mode 100644 index 0000000000..78c8ea8b9e --- /dev/null +++ b/cxx-sensors/src/test/resources/org/sonar/cxx/sensors/reports-project/pclint-reports/pclint-result-with-supplemental.xml @@ -0,0 +1,9 @@ + + + + + + + + +