summaryrefslogtreecommitdiffstats
path: root/lib/python/qmk/cli/format/c.py
blob: a58aef3fbc16a396a1093aee0f4f766701e72188 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
"""Format C code according to QMK's style.
"""
from shutil import which
from subprocess import CalledProcessError, DEVNULL, Popen, PIPE

from argcomplete.completers import FilesCompleter
from milc import cli

from qmk.path import normpath
from qmk.c_parse import c_source_files

c_file_suffixes = ('c', 'h', 'cpp', 'hpp')
core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards')


def is_relative_to(file, other):
    """Provide similar behavior to PurePath.is_relative_to in Python > 3.9
    """
    return str(normpath(file).resolve()).startswith(str(normpath(other).resolve()))


def find_clang_format():
    """Returns the path to clang-format.
    """
    for clang_version in range(20, 6, -1):
        binary = f'clang-format-{clang_version}'

        if which(binary):
            return binary

    return 'clang-format'


def find_diffs(files):
    """Run clang-format and diff it against a file.
    """
    found_diffs = False

    for file in files:
        cli.log.debug('Checking for changes in %s', file)
        clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
        diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)

        if diff.returncode != 0:
            print(diff.stdout)
            found_diffs = True

    return found_diffs


def cformat_run(files):
    """Spawn clang-format subprocess with proper arguments
    """
    # Determine which version of clang-format to use
    clang_format = [find_clang_format(), '-i']

    try:
        cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
        cli.log.info('Successfully formatted the C code.')
        return True

    except CalledProcessError as e:
        cli.log.error('Error formatting C code!')
        cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
        cli.log.debug('STDOUT:')
        cli.log.debug(e.stdout)
        cli.log.debug('STDERR:')
        cli.log.debug(e.stderr)
        return False


def filter_files(files, core_only=False):
    """Yield only files to be formatted and skip the rest
    """
    files = list(map(normpath, filter(None, files)))

    for file in files:
        if core_only:
            # The following statement checks each file to see if the file path is
            # - in the core directories
            # - not in the ignored directories
            if not any(is_relative_to(file, i) for i in core_dirs) or any(is_relative_to(file, i) for i in ignored):
                cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
                continue

        if file.suffix[1:] in c_file_suffixes:
            yield file
        else:
            cli.log.debug('Skipping file %s', file)


@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.')
@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
def format_c(cli):
    """Format C code according to QMK's style.
    """
    # Find the list of files to format
    if cli.args.files:
        files = list(filter_files(cli.args.files, cli.args.core_only))

        if not files:
            cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
            exit(0)

        if cli.args.all_files:
            cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))

    elif cli.args.all_files:
        all_files = c_source_files(core_dirs)
        files = list(filter_files(all_files, True))

    else:
        git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
        git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)

        if git_diff.returncode != 0:
            cli.log.error("Error running %s", git_diff_cmd)
            print(git_diff.stderr)
            return git_diff.returncode

        changed_files = git_diff.stdout.strip().split('\n')
        files = list(filter_files(changed_files, True))

    # Sanity check
    if not files:
        cli.log.error('No changed files detected. Use "qmk format-c -a" to format all core files')
        return False

    # Run clang-format on the files we've found
    if cli.args.dry_run:
        return not find_diffs(files)
    else:
        return cformat_run(files)