Skip to content

Commit 13a0236

Browse files
committed
added tool to sort includes
1 parent df9210e commit 13a0236

File tree

1 file changed

+136
-0
lines changed

1 file changed

+136
-0
lines changed

bin/sort_includes.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
2+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
3+
#
4+
# This code is free software; you can redistribute it and/or modify it
5+
# under the terms of the GNU General Public License version 2 only, as
6+
# published by the Free Software Foundation.
7+
#
8+
# This code is distributed in the hope that it will be useful, but WITHOUT
9+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11+
# version 2 for more details (a copy is included in the LICENSE file that
12+
# accompanied this code).
13+
#
14+
# You should have received a copy of the GNU General Public License version
15+
# 2 along with this work; if not, write to the Free Software Foundation,
16+
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
17+
#
18+
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
19+
# or visit www.oracle.com if you need additional information or have any
20+
# questions.
21+
22+
import re
23+
import os
24+
import argparse
25+
from pathlib import Path
26+
27+
include_line = r'^ *#include *(<[^>]+>|"[^"]+")[^\n]*$\n'
28+
includes_pattern = f"{include_line}(?:(?:^$\\n)*{include_line})*"
29+
if_pattern = r'(?P<if>^ *#(if|ifdef|ifndef)[^\n]*$\n)'
30+
endif_pattern = r'(?P<endif>^ *#endif[^\n]*$\n)'
31+
32+
conditional_pattern = f"{if_pattern}(?P<includes>{includes_pattern}){endif_pattern}"
33+
sys_include_prefix_pattern = r' *#include[^\n<]*<'
34+
35+
includes_re = re.compile(f'{includes_pattern}|{conditional_pattern}', re.MULTILINE)
36+
37+
def sorted_includes(block):
38+
"""
39+
Sorts the include statements in `block`.
40+
41+
:param block: source code chunk containing 1 or more include statements
42+
:return: `block` with the include statements sorted and a blank line
43+
between user and sys includes
44+
"""
45+
46+
# Replace blank lines with an include string that sorts after user
47+
# includes but before sys includes
48+
lines = [line if line else "#include <!!!!>" for line in block.splitlines()]
49+
50+
# Sort lines and undo blank line replacement
51+
lines = [line.replace("#include <!!!!>", "") for line in sorted(lines)]
52+
53+
# Join sorted lines back into a block
54+
block = "\n".join(lines) + "\n"
55+
56+
# Add missing blank link line between user and sys includes
57+
m = re.search(sys_include_prefix_pattern, block)
58+
if m and m.start() != 0 and block[m.start() - 1] != "\n":
59+
block = block[0:m.start()] + "\n" + block[m.start():]
60+
61+
return block
62+
63+
def sort_includes(path, args):
64+
"""
65+
Processes the C++ source file in `path` to sort its include statements.
66+
67+
:param path: a Path object for a C++ source file
68+
:param args: command line configuration
69+
:return: True if sorting changes were made, False otherwise
70+
"""
71+
source = path.read_text()
72+
prefix = None
73+
suffix = None
74+
75+
unconditional_includes = []
76+
conditional_includes = []
77+
last = None
78+
79+
for m in includes_re.finditer(source):
80+
if prefix is None:
81+
prefix = source[0:m.start()]
82+
suffix = ""
83+
else:
84+
suffix += source[last.end():m.start()]
85+
if m.group("if"):
86+
conditional_includes.append(m)
87+
else:
88+
unconditional_includes.append(m)
89+
last = m
90+
91+
if prefix:
92+
suffix += source[last.end():]
93+
new_source = prefix + sorted_includes("".join((f"{m.group()}" for m in unconditional_includes)))
94+
for m in conditional_includes:
95+
new_source += m.group("if")
96+
new_source += sorted_includes(m.group("includes"))
97+
new_source += m.group("endif")
98+
new_source += suffix
99+
else:
100+
new_source = source
101+
102+
if new_source != source:
103+
sorted_path = path if args.update else path.with_name(path.name + ".sorted")
104+
sorted_path.write_text(new_source)
105+
if args.verbose:
106+
print(f"Sorted includes in {path}")
107+
return True
108+
return False
109+
110+
if __name__ == "__main__":
111+
p = argparse.ArgumentParser(usage='%(prog)s dir...')
112+
p.add_argument("-v", "--verbose", action="store_true", help="verbose execution")
113+
p.add_argument("--update", action="store_true", help="update in-situ instead of writing modifications to files with '.sorted' suffix")
114+
p.add_argument("file", help="C++ file or directory containing C++ files", nargs="+")
115+
116+
args = p.parse_args()
117+
118+
modified = 0
119+
files = []
120+
for name in args.file:
121+
arg = Path(name)
122+
if arg.is_file():
123+
files.append(arg)
124+
else:
125+
for dirpath, dirnames, filenames in os.walk(arg):
126+
for filename in filenames:
127+
path = Path(dirpath).joinpath(filename)
128+
if path.suffix in (".cpp", "hpp"):
129+
files.append(path)
130+
131+
for path in files:
132+
if sort_includes(path, args):
133+
modified += 1
134+
135+
print(f"Processed {len(files)} files, modified {modified}.")
136+
raise SystemExit(modified)

0 commit comments

Comments
 (0)