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