Clean bad auto-format in decidegpuusage doxygen comments
[gromacs.git] / docs / doxygen / includesorter.py
bloba9ce6014882cc95d8be7c05f5c60f4cccf60950d
1 #!/usr/bin/env python3
3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2012,2013,2014,2015,2016,2017,2018,2019, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 # Lesser General Public License for more details.
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
36 """Include directive sorter for GROMACS.
38 This module implements an #include directive sorter for GROMACS C/C++ files.
39 It allows (in most cases) automatically sorting includes and formatting
40 the paths to use either relative paths or paths relative to src/.
41 It groups includes in groups of related headers, sorts the headers
42 alphabetically within each block, and inserts empty lines in between.
43 It can be run as a standalone script, in which case it requires an up-to-date
44 Doxygen XML documentation to be present in the
45 build tree. It can also be imported as a module to be embedded in other
46 scripts. In the latter case, the IncludeSorter provides the main interface.
48 The sorting assumes some conventions (e.g., that system headers are included
49 with angle brackets instead of quotes). Generally, these conventions are
50 checked by the check-source.py script.
52 A more detailed description can be found in the developer manual.
53 """
55 import os.path
56 import re
57 import sys
58 import functools
60 @functools.total_ordering
61 class IncludeGroup(object):
63 """Enumeration type for grouping includes."""
65 def __init__(self, value):
66 """Initialize a IncludeGroup instance.
68 IncludeGroup.{main,system_c,...} should be used outside the
69 class instead of calling the constructor.
70 """
71 self._value = value
73 def __eq__(self, other):
74 """Order include groups in the desired order."""
75 return self._value == other._value
77 def __lt__(self, other):
78 """Order include groups in the desired order."""
79 return self._value < other._value
81 # gmxpre.h is always first
82 IncludeGroup.pre = IncludeGroup(0)
83 # "main" include file for the source file is next
84 IncludeGroup.main = IncludeGroup(1)
85 # config.h is next, if present, to keep its location consistent
86 IncludeGroup.config = IncludeGroup(2)
87 # Followed by system headers, with C first and C++ following
88 IncludeGroup.system_c = IncludeGroup(3)
89 IncludeGroup.system_c_cpp = IncludeGroup(4)
90 IncludeGroup.system_cpp = IncludeGroup(5)
91 # System headers not in standard C/C++ are in a separate block
92 IncludeGroup.system_other = IncludeGroup(6)
93 # src/external/ contents that are included with quotes go here
94 IncludeGroup.nonsystem_other = IncludeGroup(7)
95 # Other GROMACS headers
96 IncludeGroup.gmx_general = IncludeGroup(8)
97 # This group is for shared (unit) testing utilities
98 IncludeGroup.gmx_test = IncludeGroup(9)
99 # This group is for headers local to the including file/module
100 IncludeGroup.gmx_local = IncludeGroup(10)
102 class GroupedSorter(object):
104 """Grouping and formatting logic for #include directives.
106 This class implements the actual logic that decides how includes are
107 grouped and sorted, and how they are formatted."""
109 # These variables contain the list of system headers for various blocks
110 _std_c_headers = ['assert.h', 'ctype.h', 'errno.h', 'float.h',
111 'inttypes.h', 'limits.h', 'math.h', 'signal.h', 'stdarg.h',
112 'stddef.h', 'stdint.h', 'stdio.h', 'stdlib.h', 'string.h',
113 'time.h']
114 _std_c_cpp_headers = ['c' + x[:-2] for x in _std_c_headers]
115 _std_cpp_headers = ['algorithm', 'array', 'chrono', 'deque', 'exception', 'fstream',
116 'functional', 'initializer_list', 'iomanip', 'ios', 'iosfwd',
117 'iostream', 'istream', 'iterator',
118 'limits', 'list', 'map', 'memory', 'mutex',
119 'new', 'numeric', 'ostream', 'random',
120 'regex', 'set', 'sstream', 'stdexcept', 'streambuf', 'string', 'strstream',
121 'thread', 'tuple', 'type_traits', 'typeindex', 'typeinfo', 'vector',
122 'unordered_map', 'utility']
124 def __init__(self, style='pub-priv', absolute=False):
125 """Initialize a sorted with the given style."""
126 if style == 'single-group':
127 self._local_group = 'none'
128 elif style == 'pub-priv':
129 self._local_group = 'private'
130 else:
131 self._local_group = 'local'
132 if absolute:
133 self._abspath_main = True
134 self._abspath_local = True
135 else:
136 self._abspath_main = False
137 self._abspath_local = False
139 def _get_path(self, included_file, group, including_file):
140 """Compute include path to use for an #include.
142 The path is made either absolute (i.e., relative to src/), or
143 relative to the location of the including file, depending on the group
144 the file is in.
146 use_abspath = including_file is None or group is None
147 if not use_abspath:
148 if group in (IncludeGroup.gmx_general, IncludeGroup.gmx_test):
149 use_abspath = True
150 elif group == IncludeGroup.main and self._abspath_main:
151 use_abspath = True
152 elif group == IncludeGroup.gmx_local and self._abspath_local:
153 use_abspath = True
154 if not use_abspath:
155 fromdir = os.path.dirname(including_file.get_abspath())
156 relpath = os.path.relpath(included_file.get_abspath(), fromdir)
157 if not relpath.startswith('..'):
158 return relpath
159 path = included_file.get_relpath()
160 assert path.startswith('src/')
161 return path[4:]
163 def _get_gmx_group(self, including_file, included_file):
164 """Determine group for GROMACS headers.
166 Helper function to determine the group for an #include directive
167 when the #include is in one of the gmx_* groups (or in the main group).
169 main_header = including_file.get_main_header()
170 if main_header and main_header == included_file:
171 return IncludeGroup.main
172 if included_file.get_directory().get_name() == 'testutils':
173 return IncludeGroup.gmx_test
174 if including_file.get_directory().contains(included_file):
175 if self._local_group == 'local':
176 return IncludeGroup.gmx_local
177 if self._local_group == 'private':
178 if included_file.api_type_is_reliable() \
179 and included_file.is_module_internal():
180 return IncludeGroup.gmx_local
181 if not included_file.api_type_is_reliable() \
182 and including_file.get_relpath().startswith('src/programs'):
183 return IncludeGroup.gmx_local
184 if included_file.is_test_file():
185 return IncludeGroup.gmx_test
186 return IncludeGroup.gmx_general
188 def _split_path(self, path):
189 """Split include path into sortable compoments.
191 Plain string on the full path in the #include directive causes some
192 unintuitive behavior, so this splits the path into a tuple at
193 points that allow more natural sorting: primary sort criterion is the
194 directory name, followed by the basename (without extension) of the
195 included file.
197 path_components = list(os.path.split(path))
198 path_components[1] = os.path.splitext(path_components[1])
199 return tuple(path_components)
201 def _join_path(self, path_components):
202 """Reconstruct path from the return value of _split_path."""
203 return os.path.join(path_components[0], ''.join(path_components[1]))
205 def get_sortable_object(self, include):
206 """Produce a sortable, opaque object for an include.
208 Includes are sorted by calling this function for each #include object,
209 and sorting the list made up of these objects (using the default
210 comparison operators). Each element from the sorted list is then
211 passed to format_include(), which extracts information from the opaque
212 object and formats the #include directive for output.
214 included_file = include.get_file()
215 if not included_file:
216 path = include.get_included_path()
217 if path in self._std_c_headers:
218 group = IncludeGroup.system_c
219 elif path in self._std_c_cpp_headers:
220 group = IncludeGroup.system_c_cpp
221 elif path in self._std_cpp_headers:
222 group = IncludeGroup.system_cpp
223 else:
224 group = IncludeGroup.system_other
225 elif included_file.is_external():
226 group = IncludeGroup.nonsystem_other
227 if 'external/' in include.get_included_path():
228 path = self._get_path(included_file, group, None)
229 else:
230 path = include.get_included_path()
231 elif included_file.get_name() == 'gmxpre.h':
232 group = IncludeGroup.pre
233 path = self._get_path(included_file, group, None)
234 elif included_file.get_name() == 'config.h':
235 group = IncludeGroup.config
236 path = self._get_path(included_file, group, None)
237 else:
238 including_file = include.get_including_file()
239 group = self._get_gmx_group(including_file, included_file)
240 path = self._get_path(included_file, group, including_file)
241 return (group, self._split_path(path), include)
243 def format_include(self, obj, prev):
244 """Format an #include directive after sorting."""
245 result = []
246 if prev:
247 if prev[0] != obj[0]:
248 # Print empty line between groups
249 result.append('\n')
250 elif prev[1] == obj[1]:
251 # Skip duplicates
252 return result
253 include = obj[2]
254 line = include.get_full_line()
255 include_re = r'^(?P<head>\s*#\s*include\s+)["<][^">]*[">](?P<tail>.*)$'
256 match = re.match(include_re, line)
257 assert match
258 if include.is_system():
259 path = '<{0}>'.format(self._join_path(obj[1]))
260 else:
261 path = '"{0}"'.format(self._join_path(obj[1]))
262 result.append('{0}{1}{2}\n'.format(match.group('head'), path, match.group('tail')))
263 return result
265 class IncludeSorter(object):
267 """High-level logic for sorting includes.
269 This class contains the high-level logic for sorting include statements.
270 The actual ordering and formatting the includes is delegated to a sort method
271 (see GroupedSorter) to keep things separated.
274 def __init__(self, sortmethod=None, quiet=True):
275 """Initialize the include sorter with the given sorter and options."""
276 if not sortmethod:
277 sortmethod = GroupedSorter()
278 self._sortmethod = sortmethod
279 self._quiet = quiet
280 self._changed = False
282 def _sort_include_block(self, block, lines):
283 """Sort a single include block.
285 Returns a new list of lines for the block.
286 If anything is changed, self._changed is set to True, and the caller
287 can check that."""
288 includes = sorted(map(self._sortmethod.get_sortable_object, block.get_includes()))
289 result = []
290 prev = None
291 current_line_number = block.get_first_line()-1
292 for include in includes:
293 newlines = self._sortmethod.format_include(include, prev)
294 result.extend(newlines)
295 if not self._changed:
296 for offset, newline in enumerate(newlines):
297 if lines[current_line_number + offset] != newline:
298 self._changed = True
299 break
300 current_line_number += len(newlines)
301 prev = include
302 return result
304 def sort_includes(self, fileobj):
305 """Sort all includes in a file."""
306 lines = fileobj.get_contents()
307 # Format into a list first:
308 # - avoid bugs or issues in the script truncating the file
309 # - can check whether anything was changed before touching the file
310 newlines = []
311 prev = 0
312 self._changed = False
313 for block in fileobj.get_include_blocks():
314 newlines.extend(lines[prev:block.get_first_line()-1])
315 newlines.extend(self._sort_include_block(block, lines))
316 # The returned values are 1-based, but indexing here is 0-based,
317 # so an explicit +1 is not needed.
318 prev = block.get_last_line()
319 if self._changed:
320 if not self._quiet:
321 sys.stderr.write('{0}: includes reformatted\n'.format(fileobj.get_relpath()))
322 newlines.extend(lines[prev:])
323 with open(fileobj.get_abspath(), 'w') as fp:
324 fp.write(''.join(newlines))
326 def check_sorted(self, fileobj):
327 """Check that includes within a file are sorted."""
328 # TODO: Make the checking work without full contents of the file
329 lines = fileobj.get_contents()
330 is_sorted = True
331 details = None
332 for block in fileobj.get_include_blocks():
333 self._changed = False
334 sorted_lines = self._sort_include_block(block, lines)
335 if self._changed:
336 is_sorted = False
337 # TODO: Do a proper diff to show the actual changes.
338 if details is None:
339 details = ["Correct order/style is:"]
340 else:
341 details.append(" ...")
342 details.extend([" " + x.rstrip() for x in sorted_lines])
343 return (is_sorted, details)
345 def main():
346 """Run the include sorter script."""
347 import os
348 import sys
350 from optparse import OptionParser
352 from gmxtree import GromacsTree
353 from reporter import Reporter
355 parser = OptionParser()
356 parser.add_option('-S', '--source-root',
357 help='Source tree root directory')
358 parser.add_option('-B', '--build-root',
359 help='Build tree root directory')
360 parser.add_option('-F', '--files',
361 help='Specify files to sort')
362 parser.add_option('-q', '--quiet', action='store_true',
363 help='Do not write status messages')
364 # This is for evaluating different options; can be removed from the final
365 # version.
366 parser.add_option('-s', '--style', type='choice', default='pub-priv',
367 choices=('single-group', 'pub-priv', 'pub-local'),
368 help='Style for GROMACS includes')
369 parser.add_option('--absolute', action='store_true',
370 help='Write all include paths relative to src/')
371 options, args = parser.parse_args()
373 filelist = args
374 if options.files:
375 if options.files == '-':
376 lines = sys.stdin.readlines()
377 else:
378 with open(options.files, 'r') as fp:
379 lines = fp.readlines()
380 filelist.extend([x.strip() for x in lines])
382 reporter = Reporter(quiet=True)
384 if not options.quiet:
385 sys.stderr.write('Scanning source tree...\n')
386 if not options.source_root:
387 sys.stderr.write('Source root required not specified.\n')
388 sys.exit(2)
389 if not options.build_root:
390 sys.stderr.write('Build root required not specified.\n')
391 sys.exit(2)
392 tree = GromacsTree(options.source_root, options.build_root, reporter)
393 files = []
394 for filename in filelist:
395 fileobj = tree.get_file(os.path.abspath(filename))
396 if not fileobj:
397 sys.stderr.write('warning: ignoring unknown file {0}\n'.format(filename))
398 continue
399 files.append(fileobj)
400 if not options.quiet:
401 sys.stderr.write('Reading source files...\n')
402 tree.scan_files(only_files=files, keep_contents=True)
403 extfiles = set(files)
404 for fileobj in files:
405 for included_file in fileobj.get_includes():
406 other_file = included_file.get_file()
407 if other_file:
408 extfiles.add(other_file)
409 if not options.quiet:
410 sys.stderr.write('Reading Doxygen XML files...\n')
411 tree.load_xml(only_files=extfiles)
413 if not options.quiet:
414 sys.stderr.write('Sorting includes...\n')
416 sorter = IncludeSorter(GroupedSorter(options.style, options.absolute), options.quiet)
418 for fileobj in files:
419 sorter.sort_includes(fileobj)
421 if __name__ == '__main__':
422 main()