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('<','<') 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 " ">]> 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"> </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 464 </div> 465 466 </div> 467 <div id="page-footer"> </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;"><--- %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