1#!/usr/bin/env python3 2# 3# Copyright The Mbed TLS Contributors 4# SPDX-License-Identifier: Apache-2.0 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); you may 7# not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17 18""" 19This script confirms that the naming of all symbols and identifiers in Mbed TLS 20are consistent with the house style and are also self-consistent. It only runs 21on Linux and macOS since it depends on nm. 22 23It contains two major Python classes, CodeParser and NameChecker. They both have 24a comprehensive "run-all" function (comprehensive_parse() and perform_checks()) 25but the individual functions can also be used for specific needs. 26 27CodeParser makes heavy use of regular expressions to parse the code, and is 28dependent on the current code formatting. Many Python C parser libraries require 29preprocessed C code, which means no macro parsing. Compiler tools are also not 30very helpful when we want the exact location in the original source (which 31becomes impossible when e.g. comments are stripped). 32 33NameChecker performs the following checks: 34 35- All exported and available symbols in the library object files, are explicitly 36 declared in the header files. This uses the nm command. 37- All macros, constants, and identifiers (function names, struct names, etc) 38 follow the required regex pattern. 39- Typo checking: All words that begin with MBED exist as macros or constants. 40 41The script returns 0 on success, 1 on test failure, and 2 if there is a script 42error. It must be run from Mbed TLS root. 43""" 44 45import abc 46import argparse 47import fnmatch 48import glob 49import textwrap 50import os 51import sys 52import traceback 53import re 54import enum 55import shutil 56import subprocess 57import logging 58 59# Naming patterns to check against. These are defined outside the NameCheck 60# class for ease of modification. 61MACRO_PATTERN = r"^(MBEDTLS|PSA)_[0-9A-Z_]*[0-9A-Z]$" 62CONSTANTS_PATTERN = MACRO_PATTERN 63IDENTIFIER_PATTERN = r"^(mbedtls|psa)_[0-9a-z_]*[0-9a-z]$" 64 65class Match(): # pylint: disable=too-few-public-methods 66 """ 67 A class representing a match, together with its found position. 68 69 Fields: 70 * filename: the file that the match was in. 71 * line: the full line containing the match. 72 * line_no: the line number. 73 * pos: a tuple of (start, end) positions on the line where the match is. 74 * name: the match itself. 75 """ 76 def __init__(self, filename, line, line_no, pos, name): 77 # pylint: disable=too-many-arguments 78 self.filename = filename 79 self.line = line 80 self.line_no = line_no 81 self.pos = pos 82 self.name = name 83 84 def __str__(self): 85 """ 86 Return a formatted code listing representation of the erroneous line. 87 """ 88 gutter = format(self.line_no, "4d") 89 underline = self.pos[0] * " " + (self.pos[1] - self.pos[0]) * "^" 90 91 return ( 92 " {0} |\n".format(" " * len(gutter)) + 93 " {0} | {1}".format(gutter, self.line) + 94 " {0} | {1}\n".format(" " * len(gutter), underline) 95 ) 96 97class Problem(abc.ABC): # pylint: disable=too-few-public-methods 98 """ 99 An abstract parent class representing a form of static analysis error. 100 It extends an Abstract Base Class, which means it is not instantiable, and 101 it also mandates certain abstract methods to be implemented in subclasses. 102 """ 103 # Class variable to control the quietness of all problems 104 quiet = False 105 def __init__(self): 106 self.textwrapper = textwrap.TextWrapper() 107 self.textwrapper.width = 80 108 self.textwrapper.initial_indent = " > " 109 self.textwrapper.subsequent_indent = " " 110 111 def __str__(self): 112 """ 113 Unified string representation method for all Problems. 114 """ 115 if self.__class__.quiet: 116 return self.quiet_output() 117 return self.verbose_output() 118 119 @abc.abstractmethod 120 def quiet_output(self): 121 """ 122 The output when --quiet is enabled. 123 """ 124 pass 125 126 @abc.abstractmethod 127 def verbose_output(self): 128 """ 129 The default output with explanation and code snippet if appropriate. 130 """ 131 pass 132 133class SymbolNotInHeader(Problem): # pylint: disable=too-few-public-methods 134 """ 135 A problem that occurs when an exported/available symbol in the object file 136 is not explicitly declared in header files. Created with 137 NameCheck.check_symbols_declared_in_header() 138 139 Fields: 140 * symbol_name: the name of the symbol. 141 """ 142 def __init__(self, symbol_name): 143 self.symbol_name = symbol_name 144 Problem.__init__(self) 145 146 def quiet_output(self): 147 return "{0}".format(self.symbol_name) 148 149 def verbose_output(self): 150 return self.textwrapper.fill( 151 "'{0}' was found as an available symbol in the output of nm, " 152 "however it was not declared in any header files." 153 .format(self.symbol_name)) 154 155class PatternMismatch(Problem): # pylint: disable=too-few-public-methods 156 """ 157 A problem that occurs when something doesn't match the expected pattern. 158 Created with NameCheck.check_match_pattern() 159 160 Fields: 161 * pattern: the expected regex pattern 162 * match: the Match object in question 163 """ 164 def __init__(self, pattern, match): 165 self.pattern = pattern 166 self.match = match 167 Problem.__init__(self) 168 169 170 def quiet_output(self): 171 return ( 172 "{0}:{1}:{2}" 173 .format(self.match.filename, self.match.line_no, self.match.name) 174 ) 175 176 def verbose_output(self): 177 return self.textwrapper.fill( 178 "{0}:{1}: '{2}' does not match the required pattern '{3}'." 179 .format( 180 self.match.filename, 181 self.match.line_no, 182 self.match.name, 183 self.pattern 184 ) 185 ) + "\n" + str(self.match) 186 187class Typo(Problem): # pylint: disable=too-few-public-methods 188 """ 189 A problem that occurs when a word using MBED doesn't appear to be defined as 190 constants nor enum values. Created with NameCheck.check_for_typos() 191 192 Fields: 193 * match: the Match object of the MBED name in question. 194 """ 195 def __init__(self, match): 196 self.match = match 197 Problem.__init__(self) 198 199 def quiet_output(self): 200 return ( 201 "{0}:{1}:{2}" 202 .format(self.match.filename, self.match.line_no, self.match.name) 203 ) 204 205 def verbose_output(self): 206 return self.textwrapper.fill( 207 "{0}:{1}: '{2}' looks like a typo. It was not found in any " 208 "macros or any enums. If this is not a typo, put " 209 "//no-check-names after it." 210 .format(self.match.filename, self.match.line_no, self.match.name) 211 ) + "\n" + str(self.match) 212 213class CodeParser(): 214 """ 215 Class for retrieving files and parsing the code. This can be used 216 independently of the checks that NameChecker performs, for example for 217 list_internal_identifiers.py. 218 """ 219 def __init__(self, log): 220 self.log = log 221 self.check_repo_path() 222 223 # Memo for storing "glob expression": set(filepaths) 224 self.files = {} 225 226 # Globally excluded filenames. 227 # Note that "*" can match directory separators in exclude lists. 228 self.excluded_files = ["*/bn_mul", "*/compat-1.3.h"] 229 230 @staticmethod 231 def check_repo_path(): 232 """ 233 Check that the current working directory is the project root, and throw 234 an exception if not. 235 """ 236 if not all(os.path.isdir(d) for d in ["include", "library", "tests"]): 237 raise Exception("This script must be run from Mbed TLS root") 238 239 def comprehensive_parse(self): 240 """ 241 Comprehensive ("default") function to call each parsing function and 242 retrieve various elements of the code, together with the source location. 243 244 Returns a dict of parsed item key to the corresponding List of Matches. 245 """ 246 self.log.info("Parsing source code...") 247 self.log.debug( 248 "The following files are excluded from the search: {}" 249 .format(str(self.excluded_files)) 250 ) 251 252 all_macros = self.parse_macros([ 253 "include/mbedtls/*.h", 254 "include/psa/*.h", 255 "library/*.h", 256 "tests/include/test/drivers/*.h", 257 "3rdparty/everest/include/everest/everest.h", 258 "3rdparty/everest/include/everest/x25519.h" 259 ]) 260 enum_consts = self.parse_enum_consts([ 261 "include/mbedtls/*.h", 262 "library/*.h", 263 "3rdparty/everest/include/everest/everest.h", 264 "3rdparty/everest/include/everest/x25519.h" 265 ]) 266 identifiers = self.parse_identifiers([ 267 "include/mbedtls/*.h", 268 "include/psa/*.h", 269 "library/*.h", 270 "3rdparty/everest/include/everest/everest.h", 271 "3rdparty/everest/include/everest/x25519.h" 272 ]) 273 mbed_words = self.parse_mbed_words([ 274 "include/mbedtls/*.h", 275 "include/psa/*.h", 276 "library/*.h", 277 "3rdparty/everest/include/everest/everest.h", 278 "3rdparty/everest/include/everest/x25519.h", 279 "library/*.c", 280 "3rdparty/everest/library/everest.c", 281 "3rdparty/everest/library/x25519.c" 282 ]) 283 symbols = self.parse_symbols() 284 285 # Remove identifier macros like mbedtls_printf or mbedtls_calloc 286 identifiers_justname = [x.name for x in identifiers] 287 actual_macros = [] 288 for macro in all_macros: 289 if macro.name not in identifiers_justname: 290 actual_macros.append(macro) 291 292 self.log.debug("Found:") 293 # Aligns the counts on the assumption that none exceeds 4 digits 294 self.log.debug(" {:4} Total Macros".format(len(all_macros))) 295 self.log.debug(" {:4} Non-identifier Macros".format(len(actual_macros))) 296 self.log.debug(" {:4} Enum Constants".format(len(enum_consts))) 297 self.log.debug(" {:4} Identifiers".format(len(identifiers))) 298 self.log.debug(" {:4} Exported Symbols".format(len(symbols))) 299 return { 300 "macros": actual_macros, 301 "enum_consts": enum_consts, 302 "identifiers": identifiers, 303 "symbols": symbols, 304 "mbed_words": mbed_words 305 } 306 307 def is_file_excluded(self, path, exclude_wildcards): 308 """Whether the given file path is excluded.""" 309 # exclude_wildcards may be None. Also, consider the global exclusions. 310 exclude_wildcards = (exclude_wildcards or []) + self.excluded_files 311 for pattern in exclude_wildcards: 312 if fnmatch.fnmatch(path, pattern): 313 return True 314 return False 315 316 def get_files(self, include_wildcards, exclude_wildcards): 317 """ 318 Get all files that match any of the UNIX-style wildcards. While the 319 check_names script is designed only for use on UNIX/macOS (due to nm), 320 this function alone would work fine on Windows even with forward slashes 321 in the wildcard. 322 323 Args: 324 * include_wildcards: a List of shell-style wildcards to match filepaths. 325 * exclude_wildcards: a List of shell-style wildcards to exclude. 326 327 Returns a List of relative filepaths. 328 """ 329 accumulator = set() 330 331 for include_wildcard in include_wildcards: 332 accumulator = accumulator.union(glob.iglob(include_wildcard)) 333 334 return list(path for path in accumulator 335 if not self.is_file_excluded(path, exclude_wildcards)) 336 337 def parse_macros(self, include, exclude=None): 338 """ 339 Parse all macros defined by #define preprocessor directives. 340 341 Args: 342 * include: A List of glob expressions to look for files through. 343 * exclude: A List of glob expressions for excluding files. 344 345 Returns a List of Match objects for the found macros. 346 """ 347 macro_regex = re.compile(r"# *define +(?P<macro>\w+)") 348 exclusions = ( 349 "asm", "inline", "EMIT", "_CRT_SECURE_NO_DEPRECATE", "MULADDC_" 350 ) 351 352 files = self.get_files(include, exclude) 353 self.log.debug("Looking for macros in {} files".format(len(files))) 354 355 macros = [] 356 for header_file in files: 357 with open(header_file, "r", encoding="utf-8") as header: 358 for line_no, line in enumerate(header): 359 for macro in macro_regex.finditer(line): 360 if macro.group("macro").startswith(exclusions): 361 continue 362 363 macros.append(Match( 364 header_file, 365 line, 366 line_no, 367 macro.span("macro"), 368 macro.group("macro"))) 369 370 return macros 371 372 def parse_mbed_words(self, include, exclude=None): 373 """ 374 Parse all words in the file that begin with MBED, in and out of macros, 375 comments, anything. 376 377 Args: 378 * include: A List of glob expressions to look for files through. 379 * exclude: A List of glob expressions for excluding files. 380 381 Returns a List of Match objects for words beginning with MBED. 382 """ 383 # Typos of TLS are common, hence the broader check below than MBEDTLS. 384 mbed_regex = re.compile(r"\bMBED.+?_[A-Z0-9_]*") 385 exclusions = re.compile(r"// *no-check-names|#error") 386 387 files = self.get_files(include, exclude) 388 self.log.debug("Looking for MBED words in {} files".format(len(files))) 389 390 mbed_words = [] 391 for filename in files: 392 with open(filename, "r", encoding="utf-8") as fp: 393 for line_no, line in enumerate(fp): 394 if exclusions.search(line): 395 continue 396 397 for name in mbed_regex.finditer(line): 398 mbed_words.append(Match( 399 filename, 400 line, 401 line_no, 402 name.span(0), 403 name.group(0))) 404 405 return mbed_words 406 407 def parse_enum_consts(self, include, exclude=None): 408 """ 409 Parse all enum value constants that are declared. 410 411 Args: 412 * include: A List of glob expressions to look for files through. 413 * exclude: A List of glob expressions for excluding files. 414 415 Returns a List of Match objects for the findings. 416 """ 417 files = self.get_files(include, exclude) 418 self.log.debug("Looking for enum consts in {} files".format(len(files))) 419 420 # Emulate a finite state machine to parse enum declarations. 421 # OUTSIDE_KEYWORD = outside the enum keyword 422 # IN_BRACES = inside enum opening braces 423 # IN_BETWEEN = between enum keyword and opening braces 424 states = enum.Enum("FSM", ["OUTSIDE_KEYWORD", "IN_BRACES", "IN_BETWEEN"]) 425 enum_consts = [] 426 for header_file in files: 427 state = states.OUTSIDE_KEYWORD 428 with open(header_file, "r", encoding="utf-8") as header: 429 for line_no, line in enumerate(header): 430 # Match typedefs and brackets only when they are at the 431 # beginning of the line -- if they are indented, they might 432 # be sub-structures within structs, etc. 433 if (state == states.OUTSIDE_KEYWORD and 434 re.search(r"^(typedef +)?enum +{", line)): 435 state = states.IN_BRACES 436 elif (state == states.OUTSIDE_KEYWORD and 437 re.search(r"^(typedef +)?enum", line)): 438 state = states.IN_BETWEEN 439 elif (state == states.IN_BETWEEN and 440 re.search(r"^{", line)): 441 state = states.IN_BRACES 442 elif (state == states.IN_BRACES and 443 re.search(r"^}", line)): 444 state = states.OUTSIDE_KEYWORD 445 elif (state == states.IN_BRACES and 446 not re.search(r"^ *#", line)): 447 enum_const = re.search(r"^ *(?P<enum_const>\w+)", line) 448 if not enum_const: 449 continue 450 451 enum_consts.append(Match( 452 header_file, 453 line, 454 line_no, 455 enum_const.span("enum_const"), 456 enum_const.group("enum_const"))) 457 458 return enum_consts 459 460 IGNORED_CHUNK_REGEX = re.compile('|'.join([ 461 r'/\*.*?\*/', # block comment entirely on one line 462 r'//.*', # line comment 463 r'(?P<string>")(?:[^\\\"]|\\.)*"', # string literal 464 ])) 465 466 def strip_comments_and_literals(self, line, in_block_comment): 467 """Strip comments and string literals from line. 468 469 Continuation lines are not supported. 470 471 If in_block_comment is true, assume that the line starts inside a 472 block comment. 473 474 Return updated values of (line, in_block_comment) where: 475 * Comments in line have been replaced by a space (or nothing at the 476 start or end of the line). 477 * String contents have been removed. 478 * in_block_comment indicates whether the line ends inside a block 479 comment that continues on the next line. 480 """ 481 482 # Terminate current multiline comment? 483 if in_block_comment: 484 m = re.search(r"\*/", line) 485 if m: 486 in_block_comment = False 487 line = line[m.end(0):] 488 else: 489 return '', True 490 491 # Remove full comments and string literals. 492 # Do it all together to handle cases like "/*" correctly. 493 # Note that continuation lines are not supported. 494 line = re.sub(self.IGNORED_CHUNK_REGEX, 495 lambda s: '""' if s.group('string') else ' ', 496 line) 497 498 # Start an unfinished comment? 499 # (If `/*` was part of a complete comment, it's already been removed.) 500 m = re.search(r"/\*", line) 501 if m: 502 in_block_comment = True 503 line = line[:m.start(0)] 504 505 return line, in_block_comment 506 507 IDENTIFIER_REGEX = re.compile('|'.join([ 508 # Match " something(a" or " *something(a". Functions. 509 # Assumptions: 510 # - function definition from return type to one of its arguments is 511 # all on one line 512 # - function definition line only contains alphanumeric, asterisk, 513 # underscore, and open bracket 514 r".* \**(\w+) *\( *\w", 515 # Match "(*something)(". 516 r".*\( *\* *(\w+) *\) *\(", 517 # Match names of named data structures. 518 r"(?:typedef +)?(?:struct|union|enum) +(\w+)(?: *{)?$", 519 # Match names of typedef instances, after closing bracket. 520 r"}? *(\w+)[;[].*", 521 ])) 522 # The regex below is indented for clarity. 523 EXCLUSION_LINES = re.compile("|".join([ 524 r"extern +\"C\"", 525 r"(typedef +)?(struct|union|enum)( *{)?$", 526 r"} *;?$", 527 r"$", 528 r"//", 529 r"#", 530 ])) 531 532 def parse_identifiers_in_file(self, header_file, identifiers): 533 """ 534 Parse all lines of a header where a function/enum/struct/union/typedef 535 identifier is declared, based on some regex and heuristics. Highly 536 dependent on formatting style. 537 538 Append found matches to the list ``identifiers``. 539 """ 540 541 with open(header_file, "r", encoding="utf-8") as header: 542 in_block_comment = False 543 # The previous line variable is used for concatenating lines 544 # when identifiers are formatted and spread across multiple 545 # lines. 546 previous_line = "" 547 548 for line_no, line in enumerate(header): 549 line, in_block_comment = \ 550 self.strip_comments_and_literals(line, in_block_comment) 551 552 if self.EXCLUSION_LINES.match(line): 553 previous_line = "" 554 continue 555 556 # If the line contains only space-separated alphanumeric 557 # characters (or underscore, asterisk, or open parenthesis), 558 # and nothing else, high chance it's a declaration that 559 # continues on the next line 560 if re.search(r"^([\w\*\(]+\s+)+$", line): 561 previous_line += line 562 continue 563 564 # If previous line seemed to start an unfinished declaration 565 # (as above), concat and treat them as one. 566 if previous_line: 567 line = previous_line.strip() + " " + line.strip() + "\n" 568 previous_line = "" 569 570 # Skip parsing if line has a space in front = heuristic to 571 # skip function argument lines (highly subject to formatting 572 # changes) 573 if line[0] == " ": 574 continue 575 576 identifier = self.IDENTIFIER_REGEX.search(line) 577 578 if not identifier: 579 continue 580 581 # Find the group that matched, and append it 582 for group in identifier.groups(): 583 if not group: 584 continue 585 586 identifiers.append(Match( 587 header_file, 588 line, 589 line_no, 590 identifier.span(), 591 group)) 592 593 def parse_identifiers(self, include, exclude=None): 594 """ 595 Parse all lines of a header where a function/enum/struct/union/typedef 596 identifier is declared, based on some regex and heuristics. Highly 597 dependent on formatting style. 598 599 Args: 600 * include: A List of glob expressions to look for files through. 601 * exclude: A List of glob expressions for excluding files. 602 603 Returns a List of Match objects with identifiers. 604 """ 605 606 files = self.get_files(include, exclude) 607 self.log.debug("Looking for identifiers in {} files".format(len(files))) 608 609 identifiers = [] 610 for header_file in files: 611 self.parse_identifiers_in_file(header_file, identifiers) 612 613 return identifiers 614 615 def parse_symbols(self): 616 """ 617 Compile the Mbed TLS libraries, and parse the TLS, Crypto, and x509 618 object files using nm to retrieve the list of referenced symbols. 619 Exceptions thrown here are rethrown because they would be critical 620 errors that void several tests, and thus needs to halt the program. This 621 is explicitly done for clarity. 622 623 Returns a List of unique symbols defined and used in the libraries. 624 """ 625 self.log.info("Compiling...") 626 symbols = [] 627 628 # Back up the config and atomically compile with the full configratuion. 629 shutil.copy( 630 "include/mbedtls/config.h", 631 "include/mbedtls/config.h.bak" 632 ) 633 try: 634 # Use check=True in all subprocess calls so that failures are raised 635 # as exceptions and logged. 636 subprocess.run( 637 ["python3", "scripts/config.py", "full"], 638 universal_newlines=True, 639 check=True 640 ) 641 my_environment = os.environ.copy() 642 my_environment["CFLAGS"] = "-fno-asynchronous-unwind-tables" 643 # Run make clean separately to lib to prevent unwanted behavior when 644 # make is invoked with parallelism. 645 subprocess.run( 646 ["make", "clean"], 647 universal_newlines=True, 648 check=True 649 ) 650 subprocess.run( 651 ["make", "lib"], 652 env=my_environment, 653 universal_newlines=True, 654 stdout=subprocess.PIPE, 655 stderr=subprocess.STDOUT, 656 check=True 657 ) 658 659 # Perform object file analysis using nm 660 symbols = self.parse_symbols_from_nm([ 661 "library/libmbedcrypto.a", 662 "library/libmbedtls.a", 663 "library/libmbedx509.a" 664 ]) 665 666 subprocess.run( 667 ["make", "clean"], 668 universal_newlines=True, 669 check=True 670 ) 671 except subprocess.CalledProcessError as error: 672 self.log.debug(error.output) 673 raise error 674 finally: 675 # Put back the original config regardless of there being errors. 676 # Works also for keyboard interrupts. 677 shutil.move( 678 "include/mbedtls/config.h.bak", 679 "include/mbedtls/config.h" 680 ) 681 682 return symbols 683 684 def parse_symbols_from_nm(self, object_files): 685 """ 686 Run nm to retrieve the list of referenced symbols in each object file. 687 Does not return the position data since it is of no use. 688 689 Args: 690 * object_files: a List of compiled object filepaths to search through. 691 692 Returns a List of unique symbols defined and used in any of the object 693 files. 694 """ 695 nm_undefined_regex = re.compile(r"^\S+: +U |^$|^\S+:$") 696 nm_valid_regex = re.compile(r"^\S+( [0-9A-Fa-f]+)* . _*(?P<symbol>\w+)") 697 exclusions = ("FStar", "Hacl") 698 699 symbols = [] 700 701 # Gather all outputs of nm 702 nm_output = "" 703 for lib in object_files: 704 nm_output += subprocess.run( 705 ["nm", "-og", lib], 706 universal_newlines=True, 707 stdout=subprocess.PIPE, 708 stderr=subprocess.STDOUT, 709 check=True 710 ).stdout 711 712 for line in nm_output.splitlines(): 713 if not nm_undefined_regex.search(line): 714 symbol = nm_valid_regex.search(line) 715 if (symbol and not symbol.group("symbol").startswith(exclusions)): 716 symbols.append(symbol.group("symbol")) 717 else: 718 self.log.error(line) 719 720 return symbols 721 722class NameChecker(): 723 """ 724 Representation of the core name checking operation performed by this script. 725 """ 726 def __init__(self, parse_result, log): 727 self.parse_result = parse_result 728 self.log = log 729 730 def perform_checks(self, quiet=False): 731 """ 732 A comprehensive checker that performs each check in order, and outputs 733 a final verdict. 734 735 Args: 736 * quiet: whether to hide detailed problem explanation. 737 """ 738 self.log.info("=============") 739 Problem.quiet = quiet 740 problems = 0 741 problems += self.check_symbols_declared_in_header() 742 743 pattern_checks = [ 744 ("macros", MACRO_PATTERN), 745 ("enum_consts", CONSTANTS_PATTERN), 746 ("identifiers", IDENTIFIER_PATTERN) 747 ] 748 for group, check_pattern in pattern_checks: 749 problems += self.check_match_pattern(group, check_pattern) 750 751 problems += self.check_for_typos() 752 753 self.log.info("=============") 754 if problems > 0: 755 self.log.info("FAIL: {0} problem(s) to fix".format(str(problems))) 756 if quiet: 757 self.log.info("Remove --quiet to see explanations.") 758 else: 759 self.log.info("Use --quiet for minimal output.") 760 return 1 761 else: 762 self.log.info("PASS") 763 return 0 764 765 def check_symbols_declared_in_header(self): 766 """ 767 Perform a check that all detected symbols in the library object files 768 are properly declared in headers. 769 Assumes parse_names_in_source() was called before this. 770 771 Returns the number of problems that need fixing. 772 """ 773 problems = [] 774 775 for symbol in self.parse_result["symbols"]: 776 found_symbol_declared = False 777 for identifier_match in self.parse_result["identifiers"]: 778 if symbol == identifier_match.name: 779 found_symbol_declared = True 780 break 781 782 if not found_symbol_declared: 783 problems.append(SymbolNotInHeader(symbol)) 784 785 self.output_check_result("All symbols in header", problems) 786 return len(problems) 787 788 def check_match_pattern(self, group_to_check, check_pattern): 789 """ 790 Perform a check that all items of a group conform to a regex pattern. 791 Assumes parse_names_in_source() was called before this. 792 793 Args: 794 * group_to_check: string key to index into self.parse_result. 795 * check_pattern: the regex to check against. 796 797 Returns the number of problems that need fixing. 798 """ 799 problems = [] 800 801 for item_match in self.parse_result[group_to_check]: 802 if not re.search(check_pattern, item_match.name): 803 problems.append(PatternMismatch(check_pattern, item_match)) 804 # Double underscore should not be used for names 805 if re.search(r".*__.*", item_match.name): 806 problems.append( 807 PatternMismatch("no double underscore allowed", item_match)) 808 809 self.output_check_result( 810 "Naming patterns of {}".format(group_to_check), 811 problems) 812 return len(problems) 813 814 def check_for_typos(self): 815 """ 816 Perform a check that all words in the soure code beginning with MBED are 817 either defined as macros, or as enum constants. 818 Assumes parse_names_in_source() was called before this. 819 820 Returns the number of problems that need fixing. 821 """ 822 problems = [] 823 824 # Set comprehension, equivalent to a list comprehension wrapped by set() 825 all_caps_names = { 826 match.name 827 for match 828 in self.parse_result["macros"] + self.parse_result["enum_consts"]} 829 typo_exclusion = re.compile(r"XXX|__|_$|^MBEDTLS_.*CONFIG_FILE$|" 830 r"MBEDTLS_TEST_LIBTESTDRIVER*") 831 832 for name_match in self.parse_result["mbed_words"]: 833 found = name_match.name in all_caps_names 834 835 # Since MBEDTLS_PSA_ACCEL_XXX defines are defined by the 836 # PSA driver, they will not exist as macros. However, they 837 # should still be checked for typos using the equivalent 838 # BUILTINs that exist. 839 if "MBEDTLS_PSA_ACCEL_" in name_match.name: 840 found = name_match.name.replace( 841 "MBEDTLS_PSA_ACCEL_", 842 "MBEDTLS_PSA_BUILTIN_") in all_caps_names 843 844 if not found and not typo_exclusion.search(name_match.name): 845 problems.append(Typo(name_match)) 846 847 self.output_check_result("Likely typos", problems) 848 return len(problems) 849 850 def output_check_result(self, name, problems): 851 """ 852 Write out the PASS/FAIL status of a performed check depending on whether 853 there were problems. 854 855 Args: 856 * name: the name of the test 857 * problems: a List of encountered Problems 858 """ 859 if problems: 860 self.log.info("{}: FAIL\n".format(name)) 861 for problem in problems: 862 self.log.warning(str(problem)) 863 else: 864 self.log.info("{}: PASS".format(name)) 865 866def main(): 867 """ 868 Perform argument parsing, and create an instance of CodeParser and 869 NameChecker to begin the core operation. 870 """ 871 parser = argparse.ArgumentParser( 872 formatter_class=argparse.RawDescriptionHelpFormatter, 873 description=( 874 "This script confirms that the naming of all symbols and identifiers " 875 "in Mbed TLS are consistent with the house style and are also " 876 "self-consistent.\n\n" 877 "Expected to be run from the MbedTLS root directory.") 878 ) 879 parser.add_argument( 880 "-v", "--verbose", 881 action="store_true", 882 help="show parse results" 883 ) 884 parser.add_argument( 885 "-q", "--quiet", 886 action="store_true", 887 help="hide unnecessary text, explanations, and highlighs" 888 ) 889 890 args = parser.parse_args() 891 892 # Configure the global logger, which is then passed to the classes below 893 log = logging.getLogger() 894 log.setLevel(logging.DEBUG if args.verbose else logging.INFO) 895 log.addHandler(logging.StreamHandler()) 896 897 try: 898 code_parser = CodeParser(log) 899 parse_result = code_parser.comprehensive_parse() 900 except Exception: # pylint: disable=broad-except 901 traceback.print_exc() 902 sys.exit(2) 903 904 name_checker = NameChecker(parse_result, log) 905 return_code = name_checker.perform_checks(quiet=args.quiet) 906 907 sys.exit(return_code) 908 909if __name__ == "__main__": 910 main() 911