/ test / lint / lint-format-strings.py
lint-format-strings.py
  1  #!/usr/bin/env python3
  2  #
  3  # Copyright (c) 2018-2022 The Bitcoin Core developers
  4  # Distributed under the MIT software license, see the accompanying
  5  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
  6  #
  7  
  8  """
  9  Lint format strings: This program checks that the number of arguments passed
 10  to a variadic format string function matches the number of format specifiers
 11  in the format string.
 12  """
 13  
 14  import subprocess
 15  import re
 16  import sys
 17  
 18  FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS = [
 19      'FatalErrorf,0',
 20      'fprintf,1',
 21      'tfm::format,1',  # Assuming tfm::::format(std::ostream&, ...
 22      'LogConnectFailure,1',
 23      'LogError,0',
 24      'LogWarning,0',
 25      'LogInfo,0',
 26      'LogDebug,1',
 27      'LogTrace,1',
 28      'LogPrint,1',
 29      'LogPrintf,0',
 30      'LogPrintfCategory,1',
 31      'LogPrintLevel,2',
 32      'printf,0',
 33      'snprintf,2',
 34      'sprintf,1',
 35      'strprintf,0',
 36      'vfprintf,1',
 37      'vprintf,1',
 38      'vsnprintf,1',
 39      'vsprintf,1',
 40      'WalletLogPrintf,0',
 41  ]
 42  RUN_LINT_FILE = 'test/lint/run-lint-format-strings.py'
 43  
 44  def check_doctest():
 45      command = [
 46          sys.executable,
 47          '-m',
 48          'doctest',
 49          RUN_LINT_FILE,
 50      ]
 51      try:
 52          subprocess.run(command, check = True)
 53      except subprocess.CalledProcessError:
 54          sys.exit(1)
 55  
 56  def get_matching_files(function_name):
 57      command = [
 58          'git',
 59          'grep',
 60          '--full-name',
 61          '-l',
 62          function_name,
 63          '--',
 64          '*.c',
 65          '*.cpp',
 66          '*.h',
 67      ]
 68      try:
 69          return subprocess.check_output(command, stderr = subprocess.STDOUT).decode('utf-8').splitlines()
 70      except subprocess.CalledProcessError as e:
 71          if e.returncode > 1: # return code is 1 when match is empty
 72              print(e.output.decode('utf-8'), end='')
 73              sys.exit(1)
 74          return []
 75  
 76  def main():
 77      exit_code = 0
 78      check_doctest()
 79      for s in FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS:
 80          function_name, skip_arguments = s.split(',')
 81          matching_files = get_matching_files(function_name)
 82  
 83          matching_files_filtered = []
 84          for matching_file in matching_files:
 85              if not re.search('^src/(leveldb|secp256k1|minisketch|tinyformat|test/fuzz/strprintf.cpp)|contrib/devtools/bitcoin-tidy/example_logprintf.cpp', matching_file):
 86                  matching_files_filtered.append(matching_file)
 87          matching_files_filtered.sort()
 88  
 89          run_lint_args = [
 90                      RUN_LINT_FILE,
 91                      '--skip-arguments',
 92                      skip_arguments,
 93                      function_name,
 94          ]
 95          run_lint_args.extend(matching_files_filtered)
 96  
 97          try:
 98              subprocess.run(run_lint_args, check = True)
 99          except subprocess.CalledProcessError:
100              exit_code = 1
101  
102      sys.exit(exit_code)
103  
104  if __name__ == '__main__':
105      main()