File: check-scg-cpp-style.py

package info (click to toggle)
bornagain 23.0-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 103,936 kB
  • sloc: cpp: 423,131; python: 40,997; javascript: 11,167; awk: 630; sh: 318; ruby: 173; xml: 130; makefile: 51; ansic: 24
file content (169 lines) | stat: -rwxr-xr-x 5,690 bytes parent folder | download | duplicates (2)
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
#!/usr/bin/env python3

"""
Various checks for certain C++ style rules that we choose for BornAgain,
in addition to the rules imposed by clang-format.

Copyright Forschungszentrum Jülich GmbH 2025.
License:  Public Domain
"""

import os, re, subprocess, sys

####################################################################################################
# generic utilities
####################################################################################################

def bash(cmd):
    # -i to respect .bashrc
    process = subprocess.Popen(['bash', '-i', '-c', cmd],
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)

    # Wait for the process to finish and get the output
    stdout, stderr = process.communicate()

    # Check if there were any errors
    if process.returncode != 0:
        print(f"Error: {stderr.decode('utf-8')}")
        sys.exit(1)
    return stdout.decode('utf-8').rstrip('\n').split('\n')

def split_fname(fn):
    """
    return stem, suffix
    """
    m = re.match(r'(.*)\.(\w+)$', fn)
    if m:
        return (m.group(1), m.group(2))
    return (fn, "")

####################################################################################################
# checks
####################################################################################################

def check_parallel_includes(stem, th, tc, errs):

    included_by_h = []
    for m in re.finditer(r'#include ["<](.*?)[">]', th):
        included_by_h.append(m.group(1))

    if len(included_by_h) == 0:
        return

    alternatives = '|'.join(included_by_h)
    if not re.search(r'#include ["<](' + alternatives + r')[">]', tc):
        return

    # found error, now generate informative message
    duplicates = []
    for inc in included_by_h:
        if re.search(r'#include ["<](' + inc + r')[">]', tc):
            duplicates.append(inc)

    errs.append(f"duplicate includes in source pair {stem}.h|cpp: {', '.join(duplicates)}")

def rm_namespace(t):
    return re.sub(r'\nnamespace ([\w:]+ )?{\n(.*\n)*?}.*', r'\nnamespace /*...*/', t)

def check_block(fn, t, name, errs):

    if name == "include":
        rex = r'(#include) .*?'
    elif name == "using":
        rex = r'(using) [^;]*?;'
        t = rm_namespace(t)
    elif name == "fwd decl":
        rex = r'(class|struct) \w+;'
        t = rm_namespace(t)
    matches = [m for m in re.finditer(r'(\n*|template.*)\n('+ rex + r'([ ]*//.*)?\n)+(\n*)', t)]

    if len(matches) > 1:

        # tolerate multiple #include blocks if they are separated by some #if or #pragma
        if name == "include" and re.search('#include.*#(if|pragma).*#include', t, re.DOTALL):
            return

        errs.append(f"several {name} blocks in file {fn}")

        return

    elif len(matches) == 0:
        return

    m = matches[0]

    if re.match(r'^template', m.group(1)):
        return
    if m.group(1) == '':
        if not re.match(rex, t):
            errs.append(f"missing blank line above {name} block in file {fn}")
        else:
            print(f'In file {fn} block {name} at head')
    elif m.group(1) != '\n':
        errs.append(f"more than one blank line above {name} block in file {fn}")
    if m.group(5) == '':
        errs.append(f"missing blank line below {name} block in file {fn}")
    elif m.group(5) != '\n':
        errs.append(f"more than one blank line below {name} block in file {fn}")

def check_fwd_decl_sorted(fn, t, errs):
    for m in re.finditer(r'((class|struct) (\w+);([ ]*//.*)?\n)+', t):
        decls_txt = m.group(0)
        decls_arr = decls_txt.rstrip('\n').split('\n')
        if decls_arr != sorted(decls_arr):
            errs.append(f"forward declarations not sorted in file {fn}")
            return

def check_no_trivial_getters_in_cppfile(fc, t, errs):
    m = re.search(r'\n\S+ (\w+::[a-z]\w*)\(\) const\n{\s*return m_\w+;\s*}', tc)
    if m:
        errs.append(f"trivial getter {m.group(1)} in file {fc}, should go to header file")

####################################################################################################
# main
####################################################################################################

if __name__ == "__main__":

    if (len(sys.argv)>1 and sys.argv[1]=="-h") or len(sys.argv)!=1:
        print("To be called without any arguments")
        sys.exit(1)

    errs = []

    allfiles = bash('find . -not \( -path ./auto -prune -o -path ./devtools -prune -o -name *ThirdParty* -prune -o -name *3rdparty* -prune -o -name *3rdparty* -prune -o -path ./build -prune -o -path ./debug -prune -o -path ./cover -prune -o -path ./tidy -prune -o -path ./qbuild -prune \) -a -type f -a \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \)')

    # tests on each file:
    for fn in allfiles:
        stem, suffix = split_fname(fn)
        with open(fn, 'r') as fd:
            t = fd.read()
        check_block(fn, t, 'include', errs)
        check_block(fn, t, 'using', errs)
        if suffix == 'h':
            check_block(fn, t, 'fwd decl', errs)
            check_fwd_decl_sorted(fn, t, errs)

    # tests on file pairs:
    for fn in allfiles:
        stem, suffix = split_fname(fn)
        if suffix != 'cpp':
            continue
        fh = stem+'.h'
        fc = fn
        if not os.path.exists(fh):
            continue
        with open(fh, 'r') as fd:
            th = fd.read()
        with open(fc, 'r') as fd:
            tc = fd.read()
        check_parallel_includes(stem, th, tc, errs)

    if len(errs) > 0:
        print("Found error(s):")
        for err in errs:
            print("- " + err)
        sys.exit(1)

    sys.exit(0)