1 /*
2                                     __
3                                    / _|
4   __ _ _   _ _ __ ___  _ __ __ _  | |_ ___  ___ ___
5  / _` | | | | '__/ _ \| '__/ _` | |  _/ _ \/ __/ __|
6 | (_| | |_| | | | (_) | | | (_| | | || (_) \__ \__ \
7  \__,_|\__,_|_|  \___/|_|  \__,_| |_| \___/|___/___/
8 
9 Copyright (C) 2018-2020 Aurora Free Open Source Software.
10 Copyright (C) 2018-2020 Luís Ferreira <luis@aurorafoss.org>
11 
12 This file is part of the Aurora Free Open Source Software. This
13 organization promote free and open source software that you can
14 redistribute and/or modify under the terms of the GNU Lesser General
15 Public License Version 3 as published by the Free Software Foundation or
16 (at your option) any later version approved by the Aurora Free Open Source
17 Software Organization. The license is available in the package root path
18 as 'LICENSE' file. Please review the following information to ensure the
19 GNU Lesser General Public License version 3 requirements will be met:
20 https://www.gnu.org/licenses/lgpl.html .
21 
22 Alternatively, this file may be used under the terms of the GNU General
23 Public License version 3 or later as published by the Free Software
24 Foundation. Please review the following information to ensure the GNU
25 General Public License requirements will be met:
26 http://www.gnu.org/licenses/gpl-3.0.html.
27 
28 NOTE: All products, services or anything associated to trademarks and
29 service marks used or referenced on this file are the property of their
30 respective companies/owners or its subsidiaries. Other names and brands
31 may be claimed as the property of others.
32 
33 For more info about intellectual property visit: aurorafoss.org or
34 directly send an email to: contact (at) aurorafoss.org .
35 */
36 
37 /++
38 LST Parser
39 
40 This file defines the LST format Parser
41 
42 Authors: Luís Ferreira <luis@aurorafoss.org>
43 Copyright: All rights reserved, Aurora Free Open Source Software
44 License: GNU Lesser General Public License (Version 3, 29 June 2007)
45 Date: 2020
46 +/
47 module liblstparse.parser;
48 
49 import std.algorithm;
50 import std.array;
51 import std.ascii;
52 import std.conv : to;
53 import std.exception;
54 import std.file;
55 import std.format;
56 import std.range.primitives;
57 import std.string;
58 import std.typecons;
59 import std.typecons;
60 
61 ///
62 class LSTFileParseException : Exception
63 {
64 	///
65 	mixin basicExceptionCtors;
66 }
67 
68 ///
69 class LSTFileMergeException : Exception
70 {
71 	///
72 	mixin basicExceptionCtors;
73 }
74 
75 /** LST File struct
76  *
77  * This defines an LST File model with all associated covered lines.
78  */
79 @safe public struct LSTFile
80 {
81 	/** LSTFile file content constructor
82 	 *
83 	 * This constructs a LSTFile using directly the content of the lst file
84 	 *
85 	 * Examples:
86 	 * --------------------
87 	 * auto text = readText("tuna.lst");
88 	 * LSTFile lst = LSTFile(text);
89 	 * --------------------
90 	 *
91 	 * Params:
92 	 *   text = content of the lst file
93 	 */
94 	@safe public this(string text)
95 	{
96 		import std.conv : to;
97 
98 		auto buf = text.chomp.splitLines;
99 
100 		if (buf.empty) // at the time, an empty file is generated from any empty .d file
101 			return;
102 
103 		enforce!LSTFileParseException(buf.length >= 2,
104 				"Minimum number of lines is 2. Probably not parsing .lst file");
105 
106 		foreach (i, ref line; buf[0 .. $ - 1])
107 		{
108 			immutable auto splittedLine = line.split("|");
109 			// check if the line is from a LST file
110 			enforce!LSTFileParseException(splittedLine.length >= 2,
111 					"'|' separator not found. Probably not parsing .lst file");
112 
113 			immutable auto covered = splittedLine.front.strip;
114 
115 			_lines ~= Line(
116 					(covered.empty) ? Nullable!(uint).init : nullable!uint(covered.to!uint),
117 					splittedLine[1 .. $].join);
118 		}
119 
120 		auto finalLine = buf.back;
121 
122 		if (!finalLine.endsWith(" has no code"))
123 		{
124 			auto s = finalLine.split("% covered");
125 			// check if it actually splits
126 			enforce!LSTFileParseException(s.length >= 2,
127 					"The last line is not well formatted: missing '% covered'");
128 
129 			auto splitted = s.front.split(" ");
130 			_totalCoverage = splitted.back.to!ubyte;
131 
132 			// check if lst is well formatted (has 'is' in splitted)
133 			enforce!LSTFileParseException(splitted[$ - 2] == "is",
134 					"The last line is not well formatted: missing ' is '");
135 			// remove ' is ' from 'filename.d is x% covered'
136 			_filename = splitted[0 .. $ - 2].join(" ");
137 		}
138 		else
139 		{
140 			_filename = finalLine.split(" has no code").front;
141 		}
142 	}
143 
144 	/**
145 	 * LSTFile constructor
146 	 *
147 	 * This constructs the LSTFile from customizable parameters
148 	 *
149 	 * Params:
150 	 *   filename = file name of the covered .d file
151 	 *   lines = list of Line representing the coverage and line content
152 	 *   totalCoverage = total reported coverage in percentage
153 	 */
154 	@safe public this(string filename, Line[] lines, Nullable!ubyte totalCoverage = Nullable!(ubyte).init)
155 	{
156 		_filename = filename;
157 		_lines = lines;
158 		_totalCoverage = totalCoverage;
159 	}
160 
161 	/** LSTFile direntry constructor
162 	 *
163 	 * This constructs a LSTFile using a DirEntry as a file
164 	 *
165 	 * Examples:
166 	 * --------------------
167 	 * LSTFile lst = LSTFile(DirEntry("tuna.lst"));
168 	 * --------------------
169 	 *
170 	 * Params:
171 	 *   file = file path
172 	 */
173 	@trusted public this(DirEntry file)
174 	in (file.isFile, "You should pass a file, not a directory!")
175 	{
176 		this(readText(file.name));
177 	}
178 
179 	/**
180 	 * Constructs an LSTFile object from a given
181 	 * file path
182 	 *
183 	 * Params:
184 	 *   filepath = file path of the lst file
185 	 * Returns: constructed LSTFile object
186 	 */
187 	public static LSTFile fromFilePath(string filepath)
188 	{
189 		return LSTFile(DirEntry(filepath));
190 	}
191 
192 	/**
193 	 * Generate the corresponding lst text file to this object
194 	 *
195 	 * Returns: generated lst file content
196 	 */
197 	public string generateLST()
198 	{
199 		auto ret = appender!string();
200 		if (_lines.empty) // at the time this is the behavior when generating lst file
201 			// from an empty .d file
202 			return "";
203 
204 		// no need to calculate totalCoverage if there's no coverable lines and
205 		// _totalCoverage is null
206 		bool nocov = true;
207 
208 		foreach (l; _lines)
209 		{
210 			auto genline = format!"|%s\n"(l.content);
211 			string prefixCov;
212 			if (l.coverage.isNull)
213 			{
214 				prefixCov = format!"%7s"(" "); // fill with spaces
215 			}
216 			else
217 			{
218 				nocov = false;
219 				if (l.coverage.get() == 0)
220 					prefixCov = format!"%07d"(0); // fill with 0s
221 				else
222 					prefixCov = format!"%7d"(l.coverage.get());
223 			}
224 
225 			ret ~= prefixCov ~ genline;
226 		}
227 
228 		if (nocov)
229 			ret ~= format!"%s has no code"(_filename);
230 		else
231 			ret ~= format!"%s is %d%% covered"(_filename, totalCoverage);
232 
233 		ret ~= newline;
234 
235 		return ret[];
236 	}
237 
238 	/**
239 	 * Merge this LSTFile object with a given one
240 	 *
241 	 * Params:
242 	 *   lstfile = lstfile object to merge with
243 	 * Returns: merged lstfile object
244 	 */
245 	public LSTFile merge(LSTFile lstfile) const
246 	in
247 	{
248 		// check if the coverage report is from the exact same
249 		// source, otherwise fail
250 		enforce!LSTFileMergeException(lstfile._filename == _filename,
251 				"should merge with the same file");
252 		enforce!LSTFileMergeException(lstfile._lines.length == _lines.length,
253 				"lines length mismatch");
254 
255 	}
256 	do
257 	{
258 		T getOr(T)(Nullable!T nullable, T t) const
259 		{
260 			if (!nullable.isNull)
261 				return nullable.get();
262 			return t;
263 		}
264 
265 		// Can't use an appender here due to deprecation warning issue
266 		// See: https://issues.dlang.org/show_bug.cgi?id=20552
267 		Line[] lines;
268 		//auto lines = appender!(Line[]);
269 
270 		foreach (idx, l; _lines)
271 		{
272 			enforce!LSTFileMergeException(l.content == lstfile._lines[idx].content,
273 					format!"content mismatch at line %s"(idx + 1));
274 			Nullable!uint cov;
275 
276 			if (!(l.coverage.isNull && lstfile._lines[idx].coverage.isNull))
277 				cov = getOr(lstfile._lines[idx].coverage, 0) + getOr(l.coverage, 0);
278 
279 			lines ~= Line(cov, l.content);
280 		}
281 
282 		return LSTFile(lstfile._filename, lines[]);
283 	}
284 
285 	/**
286 	 * Merge two given LSTFile objects
287 	 *
288 	 * Params:
289 	 *   lfile1 = first LSTFile object to merge
290 	 *   lfile2 = second LSTFile object to merge
291 	 * Returns: a merged LSTFIle object
292 	 */
293 	public static LSTFile merge(LSTFile lfile1, LSTFile lfile2)
294 	{
295 		return lfile1.merge(lfile2);
296 	}
297 
298 	/**
299 	 * Returns: Path of the covered filename
300 	 */
301 	@safe pure public string filename() const @property
302 	{
303 		return _filename;
304 	}
305 
306 	/**
307 	 * Returns: Total coverage percentage
308 	 */
309 	@safe pure public ubyte totalCoverage()
310 	{
311 		if (_totalCoverage.isNull)
312 		{
313 			auto assocArr = linesCovered();
314 			if (assocArr.empty)
315 			{
316 				_totalCoverage = nullable!ubyte(0);
317 				return 0;
318 			}
319 
320 			size_t ret;
321 			foreach (k, v; assocArr)
322 				if (v > 0)
323 					ret++;
324 
325 			// its fine to do this operation as it won't devide by 0 and its
326 			// also fine to cast this because it won't be greater than 100,
327 			// mathematically
328 			return (_totalCoverage = nullable!ubyte(
329 					cast(ubyte)((ret / cast(float) assocArr.length) * 100)
330 			)).get();
331 
332 		}
333 		return _totalCoverage.get();
334 	}
335 
336 	/**
337 	 * Returns: Associative array of coverable lines
338 	 */
339 	@safe pure public const(uint[size_t]) linesCovered() const @property
340 	{
341 		uint[size_t] ret;
342 		foreach (i, l; _lines)
343 			if (l.coverage.isNull)
344 				continue;
345 			else
346 				ret[i] = l.coverage.get();
347 
348 		return ret;
349 	}
350 
351 	/**
352 	 * Returns: Array of covered lines
353 	 */
354 	@safe pure public const(Line[]) lines() const @property
355 	{
356 		return _lines.dup;
357 	}
358 
359 	/**
360 	 * Returns: Coverage value of the covered line
361 	 */
362 	@safe pure public Line opIndex(size_t i)
363 	{
364 		return _lines[i];
365 	}
366 
367 	/**
368 	 * Coverable Line
369 	 *
370 	 * This struct defines a coverable line in the lst file.
371 	 */
372 	struct Line
373 	{
374 		/// coverage of that line (if appliable)
375 		Nullable!uint coverage;
376 
377 		/// content of the line
378 		string content;
379 	}
380 
381 	private string _filename;
382 	private Line[] _lines;
383 	private Nullable!ubyte _totalCoverage;
384 }