Skip to content

Commit 4d4abbb

Browse files
committed
scripts: Rework format-header.py
It was a bloody mess, now it's simpler and less likely to spoil your source code. Also use #pragma once instead of old-style guards, but only if no guards are found, i.e. in new files.
1 parent 1749f61 commit 4d4abbb

2 files changed

Lines changed: 131 additions & 226 deletions

File tree

Lines changed: 120 additions & 224 deletions
Original file line numberDiff line numberDiff line change
@@ -1,227 +1,123 @@
1-
import datetime
2-
import filecmp
3-
import fnmatch
4-
import os
1+
from datetime import datetime
2+
from pathlib import Path
53
import re
6-
import shutil
74
import sys
8-
import tempfile
9-
import textwrap
10-
11-
copyright_str = textwrap.dedent('''
12-
/*
13-
* Copyright (c) {} Roc Streaming authors
14-
*
15-
* This Source Code Form is subject to the terms of the Mozilla Public
16-
* License, v. 2.0. If a copy of the MPL was not distributed with this
17-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
18-
*/
19-
''').format(datetime.datetime.now().year)
20-
21-
def is_header(path):
22-
'''True if `path` is a C/C++ header file; False otherwise.'''
23-
return re.search(r'\.h$', path)
24-
25-
def is_test(path):
26-
'''True if `path` is a C/C++ test file; False otherwise.'''
27-
_, basename = os.path.split(path)
28-
for s in [basename, os.path.basename(os.path.dirname(path))]:
29-
if s.startswith('test_'):
30-
return True
31-
return False
32-
33-
def is_public_api(path):
34-
'''True if `path` is a public API C/C++ header file that resides in a
35-
public_api/include/roc/ directory.
36-
'''
37-
rootname = os.path.basename(
38-
os.path.dirname(
39-
os.path.dirname(
40-
os.path.abspath(path))))
41-
return rootname == 'public_api'
42-
43-
def make_guard(path):
44-
'''Generate include guard macro name basing on `path` to header file.
45-
The resulting macro looks like this: `PATH_TO_FILE_`.
46-
'''
47-
dirpath, basename = os.path.split(path)
48-
dirname = os.path.basename(dirpath)
49-
if is_public_api(path):
50-
if is_test(path):
51-
arr = ['roc', 'public_api', dirname, basename]
52-
else:
53-
arr = ['roc', 'public_api', basename]
54-
elif dirname == '.':
55-
arr = ['roc', basename]
5+
6+
COPYRIGHT_HEADER = """
7+
/*
8+
* Copyright (c) {year} Roc Streaming authors
9+
*
10+
* This Source Code Form is subject to the terms of the Mozilla Public
11+
* License, v. 2.0. If a copy of the MPL was not distributed with this
12+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
13+
*/
14+
""".lstrip()
15+
16+
DOXYGEN_HEADER = """
17+
//! @file {path}
18+
//! @brief TODO: short file description.
19+
""".lstrip()
20+
21+
PRAGMA_HEADER = """
22+
#pragma once
23+
""".lstrip()
24+
25+
H_REGEXP = re.compile(r'''
26+
^
27+
(?P<copyright> /\*.*?Copyright.*?\*/\n )?
28+
\n*
29+
(?P<span1> .*?\n )??
30+
\n*
31+
(?P<doxygen> //!\s*@file.*?\n //!\s*@brief.*?\n )?
32+
\n*
33+
(?P<span2> .*?\n )??
34+
\n*
35+
(?P<pragma> \#pragma\s+once\n | \#ifndef\s+ROC_.*?_H_\n \#define\s+ROC_.*?_H_\n )?
36+
\n*
37+
(?P<body> .* )
38+
$
39+
''', re.DOTALL | re.VERBOSE)
40+
41+
CPP_REGEXP = re.compile(r'''
42+
^
43+
(?P<copyright> /\*.*?Copyright.*?\*/\n )?
44+
\n*
45+
(?P<body> .* )?
46+
$
47+
''', re.DOTALL | re.VERBOSE)
48+
49+
def process_h(content, file_path, doxygen_path):
50+
m = H_REGEXP.match(content)
51+
52+
if m.group('copyright') and (m.group('doxygen') or not doxygen_path) \
53+
and m.group('pragma'):
54+
return None
55+
56+
result = ''
57+
58+
if m.group('copyright'):
59+
result += m.group('copyright') + '\n'
60+
else:
61+
result += COPYRIGHT_HEADER.format(year=datetime.now().year) + '\n'
62+
63+
if m.group('span1'):
64+
result += m.group('span1')
65+
66+
if m.group('doxygen'):
67+
result += m.group('doxygen') + '\n'
68+
elif doxygen_path:
69+
result += DOXYGEN_HEADER.format(path=doxygen_path) + '\n'
70+
71+
if m.group('span2'):
72+
result += m.group('span2')
73+
74+
if m.group('pragma'):
75+
result += m.group('pragma') + '\n'
5676
else:
57-
arr = [dirname, basename]
58-
while not arr[0].startswith('roc_') and arr[0] != 'roc':
59-
dirpath = os.path.dirname(dirpath)
60-
arr = [os.path.basename(dirpath)] + arr
61-
return '_'.join(arr).replace('.', '_').upper() + '_'
62-
63-
def make_doxygen_path(path):
64-
'''Generate doxygen `@file` command using `path` to the file.'''
65-
path = '/'.join(path.split(os.sep)) # switch to '/' separator
66-
path = re.sub(r'^\.?/', '', path) # remove leading "./"
67-
return '@file ' + path
68-
69-
def make_doxygen_brief(text):
70-
'''Generate doxygen `@brief` command with a given `text` as a payload.'''
71-
if not text.endswith('.'):
72-
text += '.'
73-
return '@brief ' + text
74-
75-
def format_file(output, path):
76-
'''Read file `path` and write its contents to the `output` stream applying
77-
formatting modifications if needed: add copyright, include gards, Doxygen
78-
documentation lines, and so on.
79-
'''
80-
81-
def fprint(s):
82-
output.write(s + '\n')
83-
84-
with open(path) as fp:
85-
lines = fp.read().splitlines()
86-
87-
has_copyright, has_doxygen, has_guard, is_autogen = False, False, False, False
88-
89-
section = 'copyright'
90-
brief = 'TODO'
91-
92-
original_lines = []
93-
body_start = True
94-
95-
if is_header(path):
96-
while re.match(r'^\s*$', lines[-1]):
97-
lines.pop()
98-
99-
if re.match(r'^\s*#\s*endif.*$', lines[-1]):
100-
lines.pop()
101-
102-
while re.match(r'^\s*$', lines[-1]):
103-
lines.pop()
104-
105-
for line in lines:
106-
m = re.search(r'@brief\s+(.*)', line)
107-
if m:
108-
brief = m.group(1)
109-
break
110-
111-
if lines and re.match(r'^//.*AUTO-GENERATED.*', lines[0]):
112-
is_autogen = True
113-
section = 'body'
114-
115-
while lines:
116-
line = lines.pop(0)
117-
118-
if section in ['copyright', 'doxygen', 'guard']:
119-
if re.match(r'^\s*$', line):
120-
continue
121-
122-
if section == 'copyright':
123-
original_lines += [line]
124-
125-
if re.match(r'^\s*/?\*', line):
126-
if re.match(r'^\s*/?\*.*AUTO-GENERATED.*', line):
127-
is_autogen = True
128-
129-
if re.match(r'^\s*/?\*\s*(Copyright|Mozilla)', line):
130-
has_copyright = True
131-
132-
if re.match(r'^\s*\*/', line):
133-
if is_autogen or has_copyright:
134-
for p in original_lines:
135-
fprint(p)
136-
fprint('')
137-
else:
138-
fprint(copyright_str.strip())
139-
fprint('')
140-
141-
if not has_copyright:
142-
for p in original_lines:
143-
fprint(p)
144-
fprint('')
145-
146-
section = 'doxygen' if is_header(path) else 'body'
147-
continue
148-
else:
149-
fprint(copyright_str.strip())
150-
fprint('')
151-
152-
lines = original_lines + lines
153-
section = 'doxygen' if is_header(path) else 'body'
154-
155-
continue
156-
157-
if section == 'doxygen':
158-
if re.match(r'^\s*/?\*', line) or re.match(r'^\s*//', line):
159-
continue
160-
161-
if re.match(r'^\s*//!', line):
162-
if not is_header(path) or is_test(path) or is_autogen:
163-
continue
164-
165-
if re.match(r'^\s*//!\s*@file', line):
166-
fprint('//! {}'.format(make_doxygen_path(path)))
167-
else:
168-
fprint(line)
169-
170-
has_doxygen = True
171-
continue
172-
else:
173-
if is_public_api(path) or is_test(path) or is_autogen:
174-
section = 'guard' if is_header(path) else 'body'
175-
else:
176-
if not has_doxygen:
177-
if is_header(path):
178-
fprint('//! {}'.format(make_doxygen_path(path)))
179-
fprint('//! {}'.format(make_doxygen_brief(brief)))
180-
section = 'guard'
181-
else:
182-
section = 'body'
183-
184-
if is_header(path):
185-
fprint('')
186-
187-
if section == 'guard':
188-
m = re.match(r'#\s*(ifndef|define)', line)
189-
if m:
190-
has_guard = True
191-
fprint('#{} {}'.format(m.group(1), make_guard(path)))
192-
continue
193-
else:
194-
if not has_guard:
195-
fprint('#ifndef {}'.format(make_guard(path)))
196-
fprint('#define {}'.format(make_guard(path)))
197-
fprint('')
198-
section = 'body'
199-
200-
if section == 'body':
201-
if body_start and re.match(r'^\s*$', line):
202-
continue
203-
body_start = False
204-
fprint(line)
205-
206-
if is_header(path):
207-
fprint('')
208-
fprint('#endif // {}'.format(make_guard(path)))
209-
210-
def walk_dir(directory, patterns):
211-
for root, dirs, files in os.walk(directory):
212-
for basename in files:
213-
for pattern in patterns:
214-
if fnmatch.fnmatch(basename, pattern):
215-
filename = os.path.join(root, basename)
216-
yield filename
217-
break
218-
219-
if len(sys.argv) > 1:
220-
os.chdir(sys.argv[1])
221-
222-
for path in walk_dir('.', ['*.h', '*.cpp']):
223-
with tempfile.NamedTemporaryFile('w+') as fp:
224-
format_file(fp, path)
225-
fp.flush()
226-
if not filecmp.cmp(path, fp.name):
227-
shutil.copy(fp.name, path)
77+
result += PRAGMA_HEADER + '\n'
78+
79+
if m.group('body'):
80+
result += m.group('body')
81+
82+
return result
83+
84+
def process_cpp(content):
85+
m = CPP_REGEXP.match(content)
86+
87+
if m.group('copyright'):
88+
return None
89+
90+
result = ''
91+
92+
if m.group('copyright'):
93+
result += m.group('copyright') + '\n'
94+
else:
95+
result += COPYRIGHT_HEADER.format(year=datetime.now().year) + '\n'
96+
97+
if m.group('body'):
98+
result += m.group('body')
99+
100+
return result
101+
102+
def process_file(file_path):
103+
file_path = Path(file_path).absolute().relative_to(Path.cwd()).as_posix()
104+
105+
if file_path.startswith('src/internal_modules/'):
106+
doxygen_path = file_path.replace('src/internal_modules/', '')
107+
else:
108+
doxygen_path = None
109+
110+
with open(file_path, 'r') as fp:
111+
content = fp.read()
112+
113+
if file_path.endswith('.h'):
114+
new_content = process_h(content, file_path, doxygen_path)
115+
else:
116+
new_content = process_cpp(content)
117+
118+
if new_content and new_content != content:
119+
with open(file_path, 'w') as fp:
120+
fp.write(new_content)
121+
122+
for file_path in sys.argv[1:]:
123+
process_file(file_path)

scripts/scons_plugin/commands.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,19 @@ def ClangFormat(env, src_dir):
2727
env.PrettyCommand('FMT', env.Dir(src_dir).path, 'yellow'))
2828

2929
def HeaderFormat(env, src_dir):
30+
exclude_files = [
31+
os.path.relpath(env.File('#'+s).srcnode().abspath)
32+
for s in open(env.File('#.fmtignore').abspath).read().split()
33+
]
34+
35+
files = env.GlobRecursive(
36+
src_dir, ['*.h', '*.cpp'],
37+
exclude=exclude_files)
38+
3039
return env.Action(
31-
'{python} scripts/scons_helpers/format-header.py {src_dir}'.format(
40+
'{python} scripts/scons_helpers/format-header.py {files}'.format(
3241
python=quote(env.GetPythonExecutable()),
33-
src_dir=quote(env.Dir(src_dir).path)),
42+
files=' '.join(map(str, files))),
3443
env.PrettyCommand('FMT', env.Dir(src_dir).path, 'yellow'))
3544

3645
def DocTest(env, path):

0 commit comments

Comments
 (0)