1 //			Copyright Brian Schott (Hackerpilot) 2012.
2 // Distributed under the Boost Software License, Version 1.0.
3 //	  (See accompanying file LICENSE_1_0.txt or copy at
4 //			http://www.boost.org/LICENSE_1_0.txt)
5 
6 module dscanner.reports;
7 
8 import std.json;
9 import std.algorithm : map;
10 import std.array : split, array, Appender, appender;
11 
12 import dscanner.analysis.base : Message, MessageSet;
13 import dscanner.analysis.stats_collector;
14 
15 class DScannerJsonReporter
16 {
17 	struct Issue
18 	{
19 		Message message;
20 		string type;
21 	}
22 
23 	private Appender!(Issue[]) _issues;
24 
25 	this()
26 	{
27 		_issues = appender!(Issue[]);
28 	}
29 
30 	void addMessageSet(MessageSet messageSet)
31 	{
32 		_issues ~= toIssues(messageSet);
33 	}
34 
35 	void addMessage(Message message, bool isError = false)
36 	{
37 		_issues ~= toIssue(message, isError);
38 	}
39 
40 	string getContent(StatsCollector stats, ulong lineOfCodeCount)
41 	{
42 		JSONValue result = [
43 			"issues" : JSONValue(_issues.data.map!(e => toJson(e)).array),
44 			"interfaceCount": JSONValue(stats.interfaceCount),
45 			"classCount": JSONValue(stats.classCount),
46 			"functionCount": JSONValue(stats.functionCount),
47 			"templateCount": JSONValue(stats.templateCount),
48 			"structCount": JSONValue(stats.structCount),
49 			"statementCount": JSONValue(stats.statementCount),
50 			"lineOfCodeCount": JSONValue(lineOfCodeCount),
51 			"undocumentedPublicSymbols": JSONValue(stats.undocumentedPublicSymbols)
52 		];
53 		return result.toPrettyString();
54 	}
55 
56 	private static JSONValue toJson(Issue issue)
57 	{
58 		// dfmt off
59 		JSONValue js = JSONValue([
60 			"key": JSONValue(issue.message.key),
61 			"fileName": JSONValue(issue.message.fileName),
62 			"line": JSONValue(issue.message.line),
63 			"column": JSONValue(issue.message.column),
64 			"message": JSONValue(issue.message.message),
65 			"type": JSONValue(issue.type)
66 		]);
67 		// dfmt on
68 
69 		if (issue.message.checkName !is null)
70 		{
71 			js["name"] = JSONValue(issue.message.checkName);
72 		}
73 
74 		return js;
75 	}
76 
77 	private static Issue[] toIssues(MessageSet messageSet)
78 	{
79 		return messageSet[].map!(e => toIssue(e)).array;
80 	}
81 
82 	private static Issue toIssue(Message message, bool isError = false)
83 	{
84 		// dfmt off
85 		Issue issue = {
86 			message: message,
87 			type : isError ? "error" : "warn"
88 		};
89 		// dfmt on
90 		return issue;
91 	}
92 }
93 
94 class SonarQubeGenericIssueDataReporter
95 {
96 	enum Type
97 	{
98 		bug = "BUG",
99 		vulnerability = "VULNERABILITY",
100 		codeSmell = "CODE_SMELL"
101 	}
102 
103 	enum Severity
104 	{
105 		blocker = "BLOCKER",
106 		critical = "CRITICAL",
107 		major = "MAJOR",
108 		minor = "MINOR",
109 		info = "INFO"
110 	}
111 
112 	struct Issue
113 	{
114 		string engineId;
115 		string ruleId;
116 		Location primaryLocation;
117 		string type;
118 		string severity;
119 		int effortMinutes;
120 		Location[] secondaryLocations;
121 	}
122 
123 	struct Location
124 	{
125 		string message;
126 		string filePath;
127 		TextRange textRange;
128 	}
129 
130 	struct TextRange
131 	{
132 		// SonarQube Generic Issue only allows specifying start line only
133 		// or the complete range, for which start and end has to differ
134 		// D-Scanner does not provide the complete range info
135 		long startLine;
136 	}
137 
138 	private Appender!(Issue[]) _issues;
139 
140 	this()
141 	{
142 		_issues = appender!(Issue[]);
143 	}
144 
145 	void addMessageSet(MessageSet messageSet)
146 	{
147 		_issues ~= toIssues(messageSet);
148 	}
149 	
150 	void addMessage(Message message, bool isError = false)
151 	{
152 		_issues ~= toIssue(message, isError);
153 	}
154 
155 	string getContent()
156 	{
157 		JSONValue result = [
158 			"issues" : JSONValue(_issues.data.map!(e => toJson(e)).array)
159 		];
160 		return result.toPrettyString();
161 	}
162 
163 	private static JSONValue toJson(Issue issue)
164 	{
165 		// dfmt off
166 		return JSONValue([
167 			"engineId": JSONValue(issue.engineId),
168 			"ruleId": JSONValue(issue.ruleId),
169 			"severity": JSONValue(issue.severity),
170 			"type": JSONValue(issue.type),
171 			"primaryLocation": JSONValue([
172 				"message": JSONValue(issue.primaryLocation.message),
173 				"filePath": JSONValue(issue.primaryLocation.filePath),
174 				"textRange": JSONValue([
175 					"startLine": JSONValue(issue.primaryLocation.textRange.startLine)
176 				]),
177 			]),
178 		]);
179 		// dfmt on
180 	}
181 
182 	private static Issue[] toIssues(MessageSet messageSet)
183 	{
184 		return messageSet[].map!(e => toIssue(e)).array;
185 	}
186 
187 	private static Issue toIssue(Message message, bool isError = false)
188 	{		
189 		// dfmt off
190 		Issue issue = {
191 			engineId: "dscanner",
192 			ruleId : message.key,
193 			severity : (isError) ? Severity.blocker : getSeverity(message.key),
194 			type : getType(message.key),
195 			primaryLocation : getPrimaryLocation(message)
196 		};
197 		// dfmt on
198 		return issue;
199 	}
200 
201 	private static Location getPrimaryLocation(Message message)
202 	{
203 		return Location(message.message, message.fileName, TextRange(message.line));
204 	}
205 
206 	private static string getSeverity(string key)
207 	{
208 		auto a = key.split(".");
209 
210 		if (a.length <= 1)
211 		{
212 			return Severity.major;
213 		}
214 		else
215 		{
216 			switch (a[1])
217 			{
218 				case "style":
219 					return Severity.minor;
220 				default:
221 					return Severity.major;
222 			}
223 		}
224 	}
225 
226 	private static string getType(string key)
227 	{
228 		auto a = key.split(".");
229 
230 		if (a.length <= 1)
231 		{
232 			return Type.bug;
233 		}
234 		else
235 		{
236 			switch (a[1])
237 			{
238 				case "style":
239 					return Type.codeSmell;
240 				default:
241 					return Type.bug;
242 			}
243 		}
244 	}
245 }