aboutsummaryrefslogtreecommitdiff
path: root/lib/python/qmk/cli/license_check.py
blob: 119a228c6d4028231deb91367a7aa493c3295e31 (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
# Copyright 2023 Nick Brassel (@tzarc)
# SPDX-License-Identifier: GPL-2.0-or-later
import re
from milc import cli
from qmk.constants import LICENSE_TEXTS
from qmk.path import normpath

L_PAREN = re.compile(r'\(\[\{\<')
R_PAREN = re.compile(r'\)\]\}\>')
PUNCTUATION = re.compile(r'[\.,;:]+')
TRASH_PREFIX = re.compile(r'^(\s|/|\*|#)+')
TRASH_SUFFIX = re.compile(r'(\s|/|\*|#|\\)+$')
SPACE = re.compile(r'\s+')
SUFFIXES = ['.c', '.h', '.cpp', '.cxx', '.hpp', '.hxx']


def _simplify_text(input):
    lines = input.lower().split('\n')
    lines = [PUNCTUATION.sub('', line) for line in lines]
    lines = [TRASH_PREFIX.sub('', line) for line in lines]
    lines = [TRASH_SUFFIX.sub('', line) for line in lines]
    lines = [SPACE.sub(' ', line) for line in lines]
    lines = [L_PAREN.sub('(', line) for line in lines]
    lines = [R_PAREN.sub(')', line) for line in lines]
    lines = [line.strip() for line in lines]
    lines = [line for line in lines if line is not None and line != '']
    return ' '.join(lines)


def _preformat_license_texts():
    # Pre-format all the licenses
    for _, long_licenses in LICENSE_TEXTS:
        for i in range(len(long_licenses)):
            long_licenses[i] = _simplify_text(long_licenses[i])


def _determine_suffix_condition(extensions):
    def _default_suffix_condition(s):
        return s in SUFFIXES

    conditional = _default_suffix_condition

    if extensions is not None and len(extensions) > 0:
        suffixes = [f'.{s}' if not s.startswith('.') else s for s in extensions]

        def _specific_suffix_condition(s):
            return s in suffixes

        conditional = _specific_suffix_condition

    return conditional


def _determine_file_list(inputs, conditional):
    check_list = set()
    for filename in inputs:
        if filename.is_dir():
            for file in sorted(filename.rglob('*')):
                if file.is_file() and conditional(file.suffix):
                    check_list.add(file)
        elif filename.is_file():
            if conditional(filename.suffix):
                check_list.add(filename)

    return list(sorted(check_list))


def _detect_license_from_file_contents(filename, absolute=False, short=False):
    data = filename.read_text(encoding='utf-8', errors='ignore')
    filename_out = str(filename.absolute()) if absolute else str(filename)

    if 'SPDX-License-Identifier:' in data:
        res = data.split('SPDX-License-Identifier:')
        license = re.split(r'\s|//|\*', res[1].strip())[0].strip()
        found = False
        for short_license, _ in LICENSE_TEXTS:
            if license.lower() == short_license.lower():
                license = short_license
                found = True
                break

        if not found:
            if short:
                print(f'{filename_out} UNKNOWN')
            else:
                cli.log.error(f'{{fg_cyan}}{filename_out}{{fg_reset}} -- unknown license, or no license detected!')
            return False

        if short:
            print(f'{filename_out} {license}')
        else:
            cli.log.info(f'{{fg_cyan}}{filename_out}{{fg_reset}} -- license detected: {license} (SPDX License Identifier)')
        return True

    else:
        simple_text = _simplify_text(data)
        for short_license, long_licenses in LICENSE_TEXTS:
            for long_license in long_licenses:
                if long_license in simple_text:
                    if short:
                        print(f'{filename_out} {short_license}')
                    else:
                        cli.log.info(f'{{fg_cyan}}{filename_out}{{fg_reset}} -- license detected: {short_license} (Full text)')
                    return True

        if short:
            print(f'{filename_out} UNKNOWN')
        else:
            cli.log.error(f'{{fg_cyan}}{filename_out}{{fg_reset}} -- unknown license, or no license detected!')

    return False


@cli.argument('inputs', nargs='*', arg_only=True, type=normpath, help='List of input files or directories.')
@cli.argument('-s', '--short', action='store_true', help='Short output.')
@cli.argument('-a', '--absolute', action='store_true', help='Print absolute paths.')
@cli.argument('-e', '--extension', arg_only=True, action='append', default=[], help='Override list of extensions. Can be specified multiple times for multiple extensions.')
@cli.subcommand('File license check.', hidden=False if cli.config.user.developer else True)
def license_check(cli):
    _preformat_license_texts()

    conditional = _determine_suffix_condition(cli.args.extension)
    check_list = _determine_file_list(cli.args.inputs, conditional)

    failed = False
    for filename in sorted(check_list):
        if not _detect_license_from_file_contents(filename, absolute=cli.args.absolute, short=cli.args.short):
            failed = True

    if failed:
        return False