diff --git a/README.md b/README.md index 0f76489..8071eb1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/dscanner/analysis/run.d b/src/dscanner/analysis/run.d index ba53ed6..5bd41bf 100644 --- a/src/dscanner/analysis/run.d +++ b/src/dscanner/analysis/run.d @@ -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); } /** diff --git a/src/dscanner/main.d b/src/dscanner/main.d index 5cad9db..3d32a54 100644 --- a/src/dscanner/main.d +++ b/src/dscanner/main.d @@ -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 ... + Specifies the format of the generated report. + --config Use the given configuration file instead of the default located in $HOME/.config/dscanner/dscanner.ini diff --git a/src/dscanner/reports.d b/src/dscanner/reports.d new file mode 100644 index 0000000..aad4737 --- /dev/null +++ b/src/dscanner/reports.d @@ -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; + } + } + } +} \ No newline at end of file