Added reportFormat sonarQubeGenericIssueData

This commit is contained in:
Andre Pany 2019-09-06 07:00:43 +02:00
parent 19e9b9093a
commit 3b6bbad9fe
4 changed files with 240 additions and 9 deletions

View File

@ -173,6 +173,15 @@ The "--report" option writes a JSON report on the static analysis checks
document above to standard output. This file is usually used by the D plugin for
SonarQube located [here](https://github.com/economicmodeling/sonar-d-plugin).
Using option "--reportFormat sonarQubeGenericIssueData" a report in a sonar-scanner
supported [Generic Issue Data format](https://docs.sonarqube.org/latest/analysis/generic-issue/) can be created.
$ dscanner --reportFormat sonarQubeGenericIssueData . > sonar-generic-issue-data.json
Reference the report filename in sonar-project.properties using key "sonar.externalIssuesReportPaths"
sonar.externalIssuesReportPaths=sonar-generic-issue-data.json
### Find Declaration
Ack, grep, and The Silver Searcher are useful for finding usages of symbols, but
their signal to noise ratio is not very good when searching for a symbol's

View File

@ -87,6 +87,7 @@ import dsymbol.conversion.second;
import dsymbol.modulecache : ModuleCache;
import dscanner.utils;
import dscanner.reports : SonarQubeGenericIssueDataReporter;
bool first = true;
@ -180,6 +181,31 @@ void generateReport(string[] fileNames, const StaticAnalysisConfig config,
writeln("}");
}
void generateSonarQubeGenericIssueDataReport(string[] fileNames, const StaticAnalysisConfig config,
ref StringCache cache, ref ModuleCache moduleCache)
{
auto reporter = new SonarQubeGenericIssueDataReporter();
auto writeMessages = delegate void(string fileName, size_t line, size_t column, string message, bool isError){
reporter.addMessage(Message(fileName, line, column, "dscanner.syntax", message), isError);
};
foreach (fileName; fileNames)
{
auto code = readFile(fileName);
// Skip files that could not be read and continue with the rest
if (code.length == 0)
continue;
RollbackAllocator r;
const(Token)[] tokens;
const Module m = parseModule(fileName, code, &r, cache, tokens, writeMessages, null, null, null);
MessageSet messageSet = analyze(fileName, m, config, moduleCache, tokens, true);
reporter.addMessageSet(messageSet);
}
writeln(reporter.getContent());
}
/**
* For multiple files
*
@ -217,24 +243,33 @@ bool analyze(string[] fileNames, const StaticAnalysisConfig config, string error
}
const(Module) parseModule(string fileName, ubyte[] code, RollbackAllocator* p,
string errorFormat, ref StringCache cache, bool report, ref const(Token)[] tokens,
ulong* linesOfCode = null, uint* errorCount = null, uint* warningCount = null)
ref StringCache cache, ref const(Token)[] tokens,
MessageDelegate dlgMessage, ulong* linesOfCode = null,
uint* errorCount = null, uint* warningCount = null)
{
import dscanner.stats : isLineOfCode;
auto writeMessages = delegate(string fileName, size_t line, size_t column, string message, bool isError){
return messageFunctionFormat(errorFormat, Message(fileName, line, column, message), isError);
};
LexerConfig config;
config.fileName = fileName;
config.stringBehavior = StringBehavior.source;
tokens = getTokensForParser(code, config, &cache);
if (linesOfCode !is null)
(*linesOfCode) += count!(a => isLineOfCode(a.type))(tokens);
return dparse.parser.parseModule(tokens, fileName, p,
return dparse.parser.parseModule(tokens, fileName, p, dlgMessage, errorCount, warningCount);
}
const(Module) parseModule(string fileName, ubyte[] code, RollbackAllocator* p,
string errorFormat, ref StringCache cache, bool report, ref const(Token)[] tokens,
ulong* linesOfCode = null, uint* errorCount = null, uint* warningCount = null)
{
auto writeMessages = delegate(string fileName, size_t line, size_t column, string message, bool isError){
return messageFunctionFormat(errorFormat, Message(fileName, line, column, message), isError);
};
return parseModule(fileName, code, p, cache, tokens,
report ? toDelegate(&messageFunctionJSON) : writeMessages,
errorCount, warningCount);
linesOfCode, errorCount, warningCount);
}
/**

View File

@ -62,6 +62,7 @@ else
bool defaultConfig;
bool report;
bool skipTests;
string reportFormat;
string symbolName;
string configLocation;
string[] importPaths;
@ -91,6 +92,7 @@ else
"declaration|d", &symbolName,
"config", &configLocation,
"report", &report,
"reportFormat", &reportFormat,
"I", &importPaths,
"version", &printVersion,
"muffinButton", &muffin,
@ -155,6 +157,9 @@ else
if (absImportPaths.length)
moduleCache.addImportPaths(absImportPaths);
if (reportFormat.length)
report = true;
immutable optionCount = count!"a"([sloc, highlight, ctags, tokenCount, syntaxCheck, ast, imports,
outline, tokenDump, styleCheck, defaultConfig, report,
symbolName !is null, etags, etagsAll, recursiveImports]);
@ -237,7 +242,21 @@ else
if (skipTests)
config.enabled2SkipTests;
if (report)
generateReport(expandArgs(args), config, cache, moduleCache);
{
switch (reportFormat)
{
default:
stderr.writeln("Unknown report format specified, using dscanner format");
goto case;
case "":
case "dscanner":
generateReport(expandArgs(args), config, cache, moduleCache);
break;
case "sonarQubeGenericIssueData":
generateSonarQubeGenericIssueDataReport(expandArgs(args), config, cache, moduleCache);
break;
}
}
else
return analyze(expandArgs(args), config, errorFormat, cache, moduleCache, true) ? 1 : 0;
}
@ -386,6 +405,9 @@ Options:
however the exit code will still be zero if errors or warnings are
found.
--reportFormat <dscanner | sonarQubeGenericIssueData>...
Specifies the format of the generated report.
--config <file>
Use the given configuration file instead of the default located in
$HOME/.config/dscanner/dscanner.ini

165
src/dscanner/reports.d Normal file
View File

@ -0,0 +1,165 @@
// Copyright Brian Schott (Hackerpilot) 2012.
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
module dscanner.reports;
import std.json;
import std.algorithm : map;
import std.array : split, array, Appender, appender;
import dscanner.analysis.base : Message, MessageSet;
class SonarQubeGenericIssueDataReporter
{
enum Type
{
bug = "BUG",
vulnerability = "VULNERABILITY",
codeSmell = "CODE_SMELL"
}
enum Severity
{
blocker = "BLOCKER",
critical = "CRITICAL",
major = "MAJOR",
minor = "MINOR",
info = "INFO"
}
struct Issue
{
string engineId;
string ruleId;
Location primaryLocation;
string type;
string severity;
int effortMinutes;
Location[] secondaryLocations;
}
struct Location
{
string message;
string filePath;
TextRange textRange;
}
struct TextRange
{
// SonarQube Generic Issue only allows specifying start line only
// or the complete range, for which start and end has to differ
// D-Scanner does not provide the complete range info
long startLine;
}
private Appender!(Issue[]) _issues;
this()
{
_issues = appender!(Issue[]);
}
void addMessageSet(MessageSet messageSet)
{
_issues ~= toIssues(messageSet);
}
void addMessage(Message message, bool isError = false)
{
_issues ~= toIssue(message, isError);
}
string getContent()
{
JSONValue result = [
"issues" : JSONValue(_issues.data.map!(e => toJson(e)).array)
];
return result.toPrettyString();
}
private static JSONValue toJson(Issue issue)
{
// dfmt off
return JSONValue([
"engineId": JSONValue(issue.engineId),
"ruleId": JSONValue(issue.ruleId),
"severity": JSONValue(issue.severity),
"type": JSONValue(issue.type),
"primaryLocation": JSONValue([
"message": JSONValue(issue.primaryLocation.message),
"filePath": JSONValue(issue.primaryLocation.filePath),
"textRange": JSONValue([
"startLine": JSONValue(issue.primaryLocation.textRange.startLine)
]),
]),
]);
// dfmt on
}
private static Issue[] toIssues(MessageSet messageSet)
{
return messageSet[].map!(e => toIssue(e)).array;
}
private static Issue toIssue(Message message, bool isError = false)
{
// dfmt off
Issue issue = {
engineId: "dscanner",
ruleId : message.key,
severity : (isError) ? Severity.blocker : getSeverity(message.key),
type : getType(message.key),
primaryLocation : getPrimaryLocation(message)
};
// dfmt on
return issue;
}
private static Location getPrimaryLocation(Message message)
{
return Location(message.message, message.fileName, TextRange(message.line));
}
private static string getSeverity(string key)
{
auto a = key.split(".");
if (a.length <= 1)
{
return Severity.major;
}
else
{
switch (a[1])
{
case "style":
return Severity.minor;
default:
return Severity.major;
}
}
}
private static string getType(string key)
{
auto a = key.split(".");
if (a.length <= 1)
{
return Type.bug;
}
else
{
switch (a[1])
{
case "style":
return Type.codeSmell;
default:
return Type.bug;
}
}
}
}