aboutsummaryrefslogtreecommitdiff
path: root/test/lint/lint-includes.py
blob: b3fa4b93034ca81abb0625a59ecb5458d63ae1fa (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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/usr/bin/env python3
#
# Copyright (c) 2018-2022 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# Check for duplicate includes.
# Guard against accidental introduction of new Boost dependencies.
# Check includes: Check for duplicate includes. Enforce bracket syntax includes.

import os
import re
import sys

from subprocess import check_output, CalledProcessError


EXCLUDED_DIRS = ["src/leveldb/",
                 "src/crc32c/",
                 "src/secp256k1/",
                 "src/minisketch/",
                ]

EXPECTED_BOOST_INCLUDES = ["boost/date_time/posix_time/posix_time.hpp",
                           "boost/multi_index/hashed_index.hpp",
                           "boost/multi_index/ordered_index.hpp",
                           "boost/multi_index/sequenced_index.hpp",
                           "boost/multi_index_container.hpp",
                           "boost/process.hpp",
                           "boost/signals2/connection.hpp",
                           "boost/signals2/optional_last_value.hpp",
                           "boost/signals2/signal.hpp",
                           "boost/test/included/unit_test.hpp",
                           "boost/test/unit_test.hpp"]


def get_toplevel():
    return check_output(["git", "rev-parse", "--show-toplevel"], universal_newlines=True, encoding="utf8").rstrip("\n")


def list_files_by_suffix(suffixes):
    exclude_args = [":(exclude)" + dir for dir in EXCLUDED_DIRS]

    files_list = check_output(["git", "ls-files", "src"] + exclude_args, universal_newlines=True, encoding="utf8").splitlines()

    return [file for file in files_list if file.endswith(suffixes)]


def find_duplicate_includes(include_list):
    tempset = set()
    duplicates = set()

    for inclusion in include_list:
        if inclusion in tempset:
            duplicates.add(inclusion)
        else:
            tempset.add(inclusion)

    return duplicates


def find_included_cpps():
    included_cpps = list()

    try:
        included_cpps = check_output(["git", "grep", "-E", r"^#include [<\"][^>\"]+\.cpp[>\"]", "--", "*.cpp", "*.h"], universal_newlines=True, encoding="utf8").splitlines()
    except CalledProcessError as e:
        if e.returncode > 1:
            raise e

    return included_cpps


def find_extra_boosts():
    included_boosts = list()
    filtered_included_boost_set = set()
    exclusion_set = set()

    try:
        included_boosts = check_output(["git", "grep", "-E", r"^#include <boost/", "--", "*.cpp", "*.h"], universal_newlines=True, encoding="utf8").splitlines()
    except CalledProcessError as e:
        if e.returncode > 1:
            raise e

    for boost in included_boosts:
        filtered_included_boost_set.add(re.findall(r'(?<=\<).+?(?=\>)', boost)[0])

    for expected_boost in EXPECTED_BOOST_INCLUDES:
        for boost in filtered_included_boost_set:
            if expected_boost in boost:
                exclusion_set.add(boost)

    extra_boosts = set(filtered_included_boost_set.difference(exclusion_set))

    return extra_boosts


def find_quote_syntax_inclusions():
    exclude_args = [":(exclude)" + dir for dir in EXCLUDED_DIRS]
    quote_syntax_inclusions = list()

    try:
        quote_syntax_inclusions = check_output(["git", "grep", r"^#include \"", "--", "*.cpp", "*.h"] + exclude_args, universal_newlines=True, encoding="utf8").splitlines()
    except CalledProcessError as e:
        if e.returncode > 1:
            raise e

    return quote_syntax_inclusions


def main():
    exit_code = 0

    os.chdir(get_toplevel())

    # Check for duplicate includes
    for filename in list_files_by_suffix((".cpp", ".h")):
        with open(filename, "r", encoding="utf8") as file:
            include_list = [line.rstrip("\n") for line in file if re.match(r"^#include", line)]

        duplicates = find_duplicate_includes(include_list)

        if duplicates:
            print(f"Duplicate include(s) in {filename}:")
            for duplicate in duplicates:
                print(duplicate)
            print("")
            exit_code = 1

    # Check if code includes .cpp-files
    included_cpps = find_included_cpps()

    if included_cpps:
        print("The following files #include .cpp files:")
        for included_cpp in included_cpps:
            print(included_cpp)
        print("")
        exit_code = 1

    # Guard against accidental introduction of new Boost dependencies
    extra_boosts = find_extra_boosts()

    if extra_boosts:
        for boost in extra_boosts:
            print(f"A new Boost dependency in the form of \"{boost}\" appears to have been introduced:")
            print(check_output(["git", "grep", boost, "--", "*.cpp", "*.h"], universal_newlines=True, encoding="utf8"))
        exit_code = 1

    # Check if Boost dependencies are no longer used
    for expected_boost in EXPECTED_BOOST_INCLUDES:
        try:
            check_output(["git", "grep", "-q", r"^#include <%s>" % expected_boost, "--", "*.cpp", "*.h"], universal_newlines=True, encoding="utf8")
        except CalledProcessError as e:
            if e.returncode > 1:
                raise e
            else:
                print(f"Good job! The Boost dependency \"{expected_boost}\" is no longer used. "
                       "Please remove it from EXPECTED_BOOST_INCLUDES in test/lint/lint-includes.py "
                       "to make sure this dependency is not accidentally reintroduced.\n")
                exit_code = 1

    # Enforce bracket syntax includes
    quote_syntax_inclusions = find_quote_syntax_inclusions()

    if quote_syntax_inclusions:
        print("Please use bracket syntax includes (\"#include <foo.h>\") instead of quote syntax includes:")
        for quote_syntax_inclusion in quote_syntax_inclusions:
            print(quote_syntax_inclusion)
        exit_code = 1

    sys.exit(exit_code)


if __name__ == "__main__":
    main()