cformat.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. """Format C code according to QMK's style.
  2. """
  3. import subprocess
  4. from os import path
  5. from shutil import which
  6. from argcomplete.completers import FilesCompleter
  7. from milc import cli
  8. from qmk.path import normpath
  9. from qmk.c_parse import c_source_files
  10. c_file_suffixes = ('c', 'h', 'cpp')
  11. core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
  12. ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
  13. def find_clang_format():
  14. """Returns the path to clang-format.
  15. """
  16. for clang_version in range(20, 6, -1):
  17. binary = f'clang-format-{clang_version}'
  18. if which(binary):
  19. return binary
  20. return 'clang-format'
  21. def find_diffs(files):
  22. """Run clang-format and diff it against a file.
  23. """
  24. found_diffs = False
  25. for file in files:
  26. cli.log.debug('Checking for changes in %s', file)
  27. clang_format = subprocess.Popen([find_clang_format(), file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
  28. diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
  29. if diff.returncode != 0:
  30. print(diff.stdout)
  31. found_diffs = True
  32. return found_diffs
  33. def cformat_run(files):
  34. """Spawn clang-format subprocess with proper arguments
  35. """
  36. # Determine which version of clang-format to use
  37. clang_format = [find_clang_format(), '-i']
  38. try:
  39. cli.run(clang_format + list(map(str, files)), check=True, capture_output=False)
  40. cli.log.info('Successfully formatted the C code.')
  41. return True
  42. except subprocess.CalledProcessError as e:
  43. cli.log.error('Error formatting C code!')
  44. cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
  45. cli.log.debug('STDOUT:')
  46. cli.log.debug(e.stdout)
  47. cli.log.debug('STDERR:')
  48. cli.log.debug(e.stderr)
  49. return False
  50. def filter_files(files, core_only=False):
  51. """Yield only files to be formatted and skip the rest
  52. """
  53. if core_only:
  54. # Filter non-core files
  55. for index, file in enumerate(files):
  56. # The following statement checks each file to see if the file path is
  57. # - in the core directories
  58. # - not in the ignored directories
  59. if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored):
  60. files[index] = None
  61. cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
  62. for file in files:
  63. if file and file.name.split('.')[-1] in c_file_suffixes:
  64. yield file
  65. else:
  66. cli.log.debug('Skipping file %s', file)
  67. @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
  68. @cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
  69. @cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
  70. @cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
  71. @cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.')
  72. @cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
  73. def cformat(cli):
  74. """Format C code according to QMK's style.
  75. """
  76. # Find the list of files to format
  77. if cli.args.files:
  78. files = list(filter_files(cli.args.files, cli.args.core_only))
  79. if not files:
  80. cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
  81. exit(0)
  82. if cli.args.all_files:
  83. cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
  84. elif cli.args.all_files:
  85. all_files = c_source_files(core_dirs)
  86. files = list(filter_files(all_files, True))
  87. else:
  88. git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
  89. git_diff = cli.run(git_diff_cmd)
  90. if git_diff.returncode != 0:
  91. cli.log.error("Error running %s", git_diff_cmd)
  92. print(git_diff.stderr)
  93. return git_diff.returncode
  94. files = []
  95. for file in git_diff.stdout.strip().split('\n'):
  96. if not any([file.startswith(ignore) for ignore in ignored]):
  97. if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
  98. files.append(file)
  99. # Sanity check
  100. if not files:
  101. cli.log.error('No changed files detected. Use "qmk cformat -a" to format all core files')
  102. return False
  103. # Run clang-format on the files we've found
  104. if cli.args.dry_run:
  105. return not find_diffs(files)
  106. else:
  107. return cformat_run(files)