1#! /usr/bin/env python
2# -*- encoding: utf-8 -*-
3# Michel Mooij, michel.mooij7@gmail.com
4
5"""
6Tool Description
7================
8This module provides a waf wrapper (i.e. waftool) around the C/C++ source code
9checking tool 'cppcheck'.
10
11See http://cppcheck.sourceforge.net/ for more information on the cppcheck tool
12itself.
13Note that many linux distributions already provide a ready to install version
14of cppcheck. On fedora, for instance, it can be installed using yum:
15
16	'sudo yum install cppcheck'
17
18
19Usage
20=====
21In order to use this waftool simply add it to the 'options' and 'configure'
22functions of your main waf script as shown in the example below:
23
24	def options(opt):
25		opt.load('cppcheck', tooldir='./waftools')
26
27	def configure(conf):
28		conf.load('cppcheck')
29
30Note that example shown above assumes that the cppcheck waftool is located in
31the sub directory named 'waftools'.
32
33When configured as shown in the example above, cppcheck will automatically
34perform a source code analysis on all C/C++ build tasks that have been
35defined in your waf build system.
36
37The example shown below for a C program will be used as input for cppcheck when
38building the task.
39
40	def build(bld):
41		bld.program(name='foo', src='foobar.c')
42
43The result of the source code analysis will be stored both as xml and html
44files in the build location for the task. Should any error be detected by
45cppcheck the build will be aborted and a link to the html report will be shown.
46By default, one index.html file is created for each task generator. A global
47index.html file can be obtained by setting the following variable
48in the configuration section:
49
50	conf.env.CPPCHECK_SINGLE_HTML = False
51
52When needed source code checking by cppcheck can be disabled per task, per
53detected error or warning for a particular task. It can be also be disabled for
54all tasks.
55
56In order to exclude a task from source code checking add the skip option to the
57task as shown below:
58
59	def build(bld):
60		bld.program(
61				name='foo',
62				src='foobar.c'
63				cppcheck_skip=True
64		)
65
66When needed problems detected by cppcheck may be suppressed using a file
67containing a list of suppression rules. The relative or absolute path to this
68file can be added to the build task as shown in the example below:
69
70		bld.program(
71				name='bar',
72				src='foobar.c',
73				cppcheck_suppress='bar.suppress'
74		)
75
76A cppcheck suppress file should contain one suppress rule per line. Each of
77these rules will be passed as an '--suppress=<rule>' argument to cppcheck.
78
79Dependencies
80================
81This waftool depends on the python pygments module, it is used for source code
82syntax highlighting when creating the html reports. see http://pygments.org/ for
83more information on this package.
84
85Remarks
86================
87The generation of the html report is originally based on the cppcheck-htmlreport.py
88script that comes shipped with the cppcheck tool.
89"""
90
91import sys
92import xml.etree.ElementTree as ElementTree
93from waflib import Task, TaskGen, Logs, Context, Options
94
95PYGMENTS_EXC_MSG= '''
96The required module 'pygments' could not be found. Please install it using your
97platform package manager (e.g. apt-get or yum), using 'pip' or 'easy_install',
98see 'http://pygments.org/download/' for installation instructions.
99'''
100
101try:
102	import pygments
103	from pygments import formatters, lexers
104except ImportError as e:
105	Logs.warn(PYGMENTS_EXC_MSG)
106	raise e
107
108
109def options(opt):
110	opt.add_option('--cppcheck-skip', dest='cppcheck_skip',
111		default=False, action='store_true',
112		help='do not check C/C++ sources (default=False)')
113
114	opt.add_option('--cppcheck-err-resume', dest='cppcheck_err_resume',
115		default=False, action='store_true',
116		help='continue in case of errors (default=False)')
117
118	opt.add_option('--cppcheck-bin-enable', dest='cppcheck_bin_enable',
119		default='warning,performance,portability,style,unusedFunction', action='store',
120		help="cppcheck option '--enable=' for binaries (default=warning,performance,portability,style,unusedFunction)")
121
122	opt.add_option('--cppcheck-lib-enable', dest='cppcheck_lib_enable',
123		default='warning,performance,portability,style', action='store',
124		help="cppcheck option '--enable=' for libraries (default=warning,performance,portability,style)")
125
126	opt.add_option('--cppcheck-std-c', dest='cppcheck_std_c',
127		default='c99', action='store',
128		help='cppcheck standard to use when checking C (default=c99)')
129
130	opt.add_option('--cppcheck-std-cxx', dest='cppcheck_std_cxx',
131		default='c++03', action='store',
132		help='cppcheck standard to use when checking C++ (default=c++03)')
133
134	opt.add_option('--cppcheck-check-config', dest='cppcheck_check_config',
135		default=False, action='store_true',
136		help='forced check for missing buildin include files, e.g. stdio.h (default=False)')
137
138	opt.add_option('--cppcheck-max-configs', dest='cppcheck_max_configs',
139		default='20', action='store',
140		help='maximum preprocessor (--max-configs) define iterations (default=20)')
141
142	opt.add_option('--cppcheck-jobs', dest='cppcheck_jobs',
143		default='1', action='store',
144		help='number of jobs (-j) to do the checking work (default=1)')
145
146def configure(conf):
147	if conf.options.cppcheck_skip:
148		conf.env.CPPCHECK_SKIP = [True]
149	conf.env.CPPCHECK_STD_C = conf.options.cppcheck_std_c
150	conf.env.CPPCHECK_STD_CXX = conf.options.cppcheck_std_cxx
151	conf.env.CPPCHECK_MAX_CONFIGS = conf.options.cppcheck_max_configs
152	conf.env.CPPCHECK_BIN_ENABLE = conf.options.cppcheck_bin_enable
153	conf.env.CPPCHECK_LIB_ENABLE = conf.options.cppcheck_lib_enable
154	conf.env.CPPCHECK_JOBS = conf.options.cppcheck_jobs
155	if conf.options.cppcheck_jobs != '1' and ('unusedFunction' in conf.options.cppcheck_bin_enable or 'unusedFunction' in conf.options.cppcheck_lib_enable or 'all' in conf.options.cppcheck_bin_enable or 'all' in conf.options.cppcheck_lib_enable):
156		Logs.warn('cppcheck: unusedFunction cannot be used with multiple threads, cppcheck will disable it automatically')
157	conf.find_program('cppcheck', var='CPPCHECK')
158
159	# set to True to get a single index.html file
160	conf.env.CPPCHECK_SINGLE_HTML = False
161
162@TaskGen.feature('c')
163@TaskGen.feature('cxx')
164def cppcheck_execute(self):
165	if hasattr(self.bld, 'conf'):
166		return
167	if len(self.env.CPPCHECK_SKIP) or Options.options.cppcheck_skip:
168		return
169	if getattr(self, 'cppcheck_skip', False):
170		return
171	task = self.create_task('cppcheck')
172	task.cmd = _tgen_create_cmd(self)
173	task.fatal = []
174	if not Options.options.cppcheck_err_resume:
175		task.fatal.append('error')
176
177
178def _tgen_create_cmd(self):
179	features = getattr(self, 'features', [])
180	std_c = self.env.CPPCHECK_STD_C
181	std_cxx = self.env.CPPCHECK_STD_CXX
182	max_configs = self.env.CPPCHECK_MAX_CONFIGS
183	bin_enable = self.env.CPPCHECK_BIN_ENABLE
184	lib_enable = self.env.CPPCHECK_LIB_ENABLE
185	jobs = self.env.CPPCHECK_JOBS
186
187	cmd  = self.env.CPPCHECK
188	args = ['--inconclusive','--report-progress','--verbose','--xml','--xml-version=2']
189	args.append('--max-configs=%s' % max_configs)
190	args.append('-j %s' % jobs)
191
192	if 'cxx' in features:
193		args.append('--language=c++')
194		args.append('--std=%s' % std_cxx)
195	else:
196		args.append('--language=c')
197		args.append('--std=%s' % std_c)
198
199	if Options.options.cppcheck_check_config:
200		args.append('--check-config')
201
202	if set(['cprogram','cxxprogram']) & set(features):
203		args.append('--enable=%s' % bin_enable)
204	else:
205		args.append('--enable=%s' % lib_enable)
206
207	for src in self.to_list(getattr(self, 'source', [])):
208		if not isinstance(src, str):
209			src = repr(src)
210		args.append(src)
211	for inc in self.to_incnodes(self.to_list(getattr(self, 'includes', []))):
212		if not isinstance(inc, str):
213			inc = repr(inc)
214		args.append('-I%s' % inc)
215	for inc in self.to_incnodes(self.to_list(self.env.INCLUDES)):
216		if not isinstance(inc, str):
217			inc = repr(inc)
218		args.append('-I%s' % inc)
219	return cmd + args
220
221
222class cppcheck(Task.Task):
223	quiet = True
224
225	def run(self):
226		stderr = self.generator.bld.cmd_and_log(self.cmd, quiet=Context.STDERR, output=Context.STDERR)
227		self._save_xml_report(stderr)
228		defects = self._get_defects(stderr)
229		index = self._create_html_report(defects)
230		self._errors_evaluate(defects, index)
231		return 0
232
233	def _save_xml_report(self, s):
234		'''use cppcheck xml result string, add the command string used to invoke cppcheck
235		and save as xml file.
236		'''
237		header = '%s\n' % s.splitlines()[0]
238		root = ElementTree.fromstring(s)
239		cmd = ElementTree.SubElement(root.find('cppcheck'), 'cmd')
240		cmd.text = str(self.cmd)
241		body = ElementTree.tostring(root).decode('us-ascii')
242		body_html_name = 'cppcheck-%s.xml' % self.generator.get_name()
243		if self.env.CPPCHECK_SINGLE_HTML:
244			body_html_name = 'cppcheck.xml'
245		node = self.generator.path.get_bld().find_or_declare(body_html_name)
246		node.write(header + body)
247
248	def _get_defects(self, xml_string):
249		'''evaluate the xml string returned by cppcheck (on sdterr) and use it to create
250		a list of defects.
251		'''
252		defects = []
253		for error in ElementTree.fromstring(xml_string).iter('error'):
254			defect = {}
255			defect['id'] = error.get('id')
256			defect['severity'] = error.get('severity')
257			defect['msg'] = str(error.get('msg')).replace('<','&lt;')
258			defect['verbose'] = error.get('verbose')
259			for location in error.findall('location'):
260				defect['file'] = location.get('file')
261				defect['line'] = str(int(location.get('line')) - 1)
262			defects.append(defect)
263		return defects
264
265	def _create_html_report(self, defects):
266		files, css_style_defs = self._create_html_files(defects)
267		index = self._create_html_index(files)
268		self._create_css_file(css_style_defs)
269		return index
270
271	def _create_html_files(self, defects):
272		sources = {}
273		defects = [defect for defect in defects if 'file' in defect]
274		for defect in defects:
275			name = defect['file']
276			if not name in sources:
277				sources[name] = [defect]
278			else:
279				sources[name].append(defect)
280
281		files = {}
282		css_style_defs = None
283		bpath = self.generator.path.get_bld().abspath()
284		names = list(sources.keys())
285		for i in range(0,len(names)):
286			name = names[i]
287			if self.env.CPPCHECK_SINGLE_HTML:
288				htmlfile = 'cppcheck/%i.html' % (i)
289			else:
290				htmlfile = 'cppcheck/%s%i.html' % (self.generator.get_name(),i)
291			errors = sources[name]
292			files[name] = { 'htmlfile': '%s/%s' % (bpath, htmlfile), 'errors': errors }
293			css_style_defs = self._create_html_file(name, htmlfile, errors)
294		return files, css_style_defs
295
296	def _create_html_file(self, sourcefile, htmlfile, errors):
297		name = self.generator.get_name()
298		root = ElementTree.fromstring(CPPCHECK_HTML_FILE)
299		title = root.find('head/title')
300		title.text = 'cppcheck - report - %s' % name
301
302		body = root.find('body')
303		for div in body.findall('div'):
304			if div.get('id') == 'page':
305				page = div
306				break
307		for div in page.findall('div'):
308			if div.get('id') == 'header':
309				h1 = div.find('h1')
310				h1.text = 'cppcheck report - %s' % name
311			if div.get('id') == 'menu':
312				indexlink = div.find('a')
313				if self.env.CPPCHECK_SINGLE_HTML:
314					indexlink.attrib['href'] = 'index.html'
315				else:
316					indexlink.attrib['href'] = 'index-%s.html' % name
317			if div.get('id') == 'content':
318				content = div
319				srcnode = self.generator.bld.root.find_node(sourcefile)
320				hl_lines = [e['line'] for e in errors if 'line' in e]
321				formatter = CppcheckHtmlFormatter(linenos=True, style='colorful', hl_lines=hl_lines, lineanchors='line')
322				formatter.errors = [e for e in errors if 'line' in e]
323				css_style_defs = formatter.get_style_defs('.highlight')
324				lexer = pygments.lexers.guess_lexer_for_filename(sourcefile, "")
325				s = pygments.highlight(srcnode.read(), lexer, formatter)
326				table = ElementTree.fromstring(s)
327				content.append(table)
328
329		s = ElementTree.tostring(root, method='html').decode('us-ascii')
330		s = CCPCHECK_HTML_TYPE + s
331		node = self.generator.path.get_bld().find_or_declare(htmlfile)
332		node.write(s)
333		return css_style_defs
334
335	def _create_html_index(self, files):
336		name = self.generator.get_name()
337		root = ElementTree.fromstring(CPPCHECK_HTML_FILE)
338		title = root.find('head/title')
339		title.text = 'cppcheck - report - %s' % name
340
341		body = root.find('body')
342		for div in body.findall('div'):
343			if div.get('id') == 'page':
344				page = div
345				break
346		for div in page.findall('div'):
347			if div.get('id') == 'header':
348				h1 = div.find('h1')
349				h1.text = 'cppcheck report - %s' % name
350			if div.get('id') == 'content':
351				content = div
352				self._create_html_table(content, files)
353			if div.get('id') == 'menu':
354				indexlink = div.find('a')
355				if self.env.CPPCHECK_SINGLE_HTML:
356					indexlink.attrib['href'] = 'index.html'
357				else:
358					indexlink.attrib['href'] = 'index-%s.html' % name
359
360		s = ElementTree.tostring(root, method='html').decode('us-ascii')
361		s = CCPCHECK_HTML_TYPE + s
362		index_html_name = 'cppcheck/index-%s.html' % name
363		if self.env.CPPCHECK_SINGLE_HTML:
364			index_html_name = 'cppcheck/index.html'
365		node = self.generator.path.get_bld().find_or_declare(index_html_name)
366		node.write(s)
367		return node
368
369	def _create_html_table(self, content, files):
370		table = ElementTree.fromstring(CPPCHECK_HTML_TABLE)
371		for name, val in files.items():
372			f = val['htmlfile']
373			s = '<tr><td colspan="4"><a href="%s">%s</a></td></tr>\n' % (f,name)
374			row = ElementTree.fromstring(s)
375			table.append(row)
376
377			errors = sorted(val['errors'], key=lambda e: int(e['line']) if 'line' in e else sys.maxint)
378			for e in errors:
379				if not 'line' in e:
380					s = '<tr><td></td><td>%s</td><td>%s</td><td>%s</td></tr>\n' % (e['id'], e['severity'], e['msg'])
381				else:
382					attr = ''
383					if e['severity'] == 'error':
384						attr = 'class="error"'
385					s = '<tr><td><a href="%s#line-%s">%s</a></td>' % (f, e['line'], e['line'])
386					s+= '<td>%s</td><td>%s</td><td %s>%s</td></tr>\n' % (e['id'], e['severity'], attr, e['msg'])
387				row = ElementTree.fromstring(s)
388				table.append(row)
389		content.append(table)
390
391	def _create_css_file(self, css_style_defs):
392		css = str(CPPCHECK_CSS_FILE)
393		if css_style_defs:
394			css = "%s\n%s\n" % (css, css_style_defs)
395		node = self.generator.path.get_bld().find_or_declare('cppcheck/style.css')
396		node.write(css)
397
398	def _errors_evaluate(self, errors, http_index):
399		name = self.generator.get_name()
400		fatal = self.fatal
401		severity = [err['severity'] for err in errors]
402		problems = [err for err in errors if err['severity'] != 'information']
403
404		if set(fatal) & set(severity):
405			exc  = "\n"
406			exc += "\nccpcheck detected fatal error(s) in task '%s', see report for details:" % name
407			exc += "\n    file://%r" % (http_index)
408			exc += "\n"
409			self.generator.bld.fatal(exc)
410
411		elif len(problems):
412			msg =  "\nccpcheck detected (possible) problem(s) in task '%s', see report for details:" % name
413			msg += "\n    file://%r" % http_index
414			msg += "\n"
415			Logs.error(msg)
416
417
418class CppcheckHtmlFormatter(pygments.formatters.HtmlFormatter):
419	errors = []
420
421	def wrap(self, source, outfile):
422		line_no = 1
423		for i, t in super(CppcheckHtmlFormatter, self).wrap(source, outfile):
424			# If this is a source code line we want to add a span tag at the end.
425			if i == 1:
426				for error in self.errors:
427					if int(error['line']) == line_no:
428						t = t.replace('\n', CPPCHECK_HTML_ERROR % error['msg'])
429				line_no += 1
430			yield i, t
431
432
433CCPCHECK_HTML_TYPE = \
434'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n'
435
436CPPCHECK_HTML_FILE = """
437<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd" [<!ENTITY nbsp "&#160;">]>
438<html>
439	<head>
440		<title>cppcheck - report - XXX</title>
441		<link href="style.css" rel="stylesheet" type="text/css" />
442		<style type="text/css">
443		</style>
444	</head>
445	<body class="body">
446		<div id="page-header">&nbsp;</div>
447		<div id="page">
448			<div id="header">
449				<h1>cppcheck report - XXX</h1>
450			</div>
451			<div id="menu">
452				<a href="index.html">Defect list</a>
453			</div>
454			<div id="content">
455			</div>
456			<div id="footer">
457				<div>cppcheck - a tool for static C/C++ code analysis</div>
458				<div>
459				Internet: <a href="http://cppcheck.sourceforge.net">http://cppcheck.sourceforge.net</a><br/>
460          		Forum: <a href="http://apps.sourceforge.net/phpbb/cppcheck/">http://apps.sourceforge.net/phpbb/cppcheck/</a><br/>
461				IRC: #cppcheck at irc.freenode.net
462				</div>
463				&nbsp;
464			</div>
465		&nbsp;
466		</div>
467		<div id="page-footer">&nbsp;</div>
468	</body>
469</html>
470"""
471
472CPPCHECK_HTML_TABLE = """
473<table>
474	<tr>
475		<th>Line</th>
476		<th>Id</th>
477		<th>Severity</th>
478		<th>Message</th>
479	</tr>
480</table>
481"""
482
483CPPCHECK_HTML_ERROR = \
484'<span style="background: #ffaaaa;padding: 3px;">&lt;--- %s</span>\n'
485
486CPPCHECK_CSS_FILE = """
487body.body {
488	font-family: Arial;
489	font-size: 13px;
490	background-color: black;
491	padding: 0px;
492	margin: 0px;
493}
494
495.error {
496	font-family: Arial;
497	font-size: 13px;
498	background-color: #ffb7b7;
499	padding: 0px;
500	margin: 0px;
501}
502
503th, td {
504	min-width: 100px;
505	text-align: left;
506}
507
508#page-header {
509	clear: both;
510	width: 1200px;
511	margin: 20px auto 0px auto;
512	height: 10px;
513	border-bottom-width: 2px;
514	border-bottom-style: solid;
515	border-bottom-color: #aaaaaa;
516}
517
518#page {
519	width: 1160px;
520	margin: auto;
521	border-left-width: 2px;
522	border-left-style: solid;
523	border-left-color: #aaaaaa;
524	border-right-width: 2px;
525	border-right-style: solid;
526	border-right-color: #aaaaaa;
527	background-color: White;
528	padding: 20px;
529}
530
531#page-footer {
532	clear: both;
533	width: 1200px;
534	margin: auto;
535	height: 10px;
536	border-top-width: 2px;
537	border-top-style: solid;
538	border-top-color: #aaaaaa;
539}
540
541#header {
542	width: 100%;
543	height: 70px;
544	background-image: url(logo.png);
545	background-repeat: no-repeat;
546	background-position: left top;
547	border-bottom-style: solid;
548	border-bottom-width: thin;
549	border-bottom-color: #aaaaaa;
550}
551
552#menu {
553	margin-top: 5px;
554	text-align: left;
555	float: left;
556	width: 100px;
557	height: 300px;
558}
559
560#menu > a {
561	margin-left: 10px;
562	display: block;
563}
564
565#content {
566	float: left;
567	width: 1020px;
568	margin: 5px;
569	padding: 0px 10px 10px 10px;
570	border-left-style: solid;
571	border-left-width: thin;
572	border-left-color: #aaaaaa;
573}
574
575#footer {
576	padding-bottom: 5px;
577	padding-top: 5px;
578	border-top-style: solid;
579	border-top-width: thin;
580	border-top-color: #aaaaaa;
581	clear: both;
582	font-size: 10px;
583}
584
585#footer > div {
586	float: left;
587	width: 33%;
588}
589
590"""
591
592