3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2012,2013,2014,2015,2016 by the GROMACS development team.
6 # Copyright (c) 2017,2018,2019,2020, by the GROMACS development team, led by
7 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
8 # and including many others, as listed in the AUTHORS file in the
9 # top-level source directory and at http://www.gromacs.org.
11 # GROMACS is free software; you can redistribute it and/or
12 # modify it under the terms of the GNU Lesser General Public License
13 # as published by the Free Software Foundation; either version 2.1
14 # of the License, or (at your option) any later version.
16 # GROMACS is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 # Lesser General Public License for more details.
21 # You should have received a copy of the GNU Lesser General Public
22 # License along with GROMACS; if not, see
23 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
24 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 # If you want to redistribute modifications to GROMACS, please
27 # consider that scientific software is very special. Version
28 # control is crucial - bugs must be traceable. We will be happy to
29 # consider code for inclusion in the official distribution, but
30 # derived work must not be called official GROMACS. Details are found
31 # in the README & COPYING files - if they are missing, get the
32 # official version at http://www.gromacs.org.
34 # To help us fund GROMACS development, we humbly ask that you cite
35 # the research papers on the package. Check out http://www.gromacs.org.
37 """Include directive sorter for GROMACS.
39 This module implements an #include directive sorter for GROMACS C/C++ files.
40 It allows (in most cases) automatically sorting includes and formatting
41 the paths to use either relative paths or paths relative to src/.
42 It groups includes in groups of related headers, sorts the headers
43 alphabetically within each block, and inserts empty lines in between.
44 It can be run as a standalone script, in which case it requires an up-to-date
45 Doxygen XML documentation to be present in the
46 build tree. It can also be imported as a module to be embedded in other
47 scripts. In the latter case, the IncludeSorter provides the main interface.
49 The sorting assumes some conventions (e.g., that system headers are included
50 with angle brackets instead of quotes). Generally, these conventions are
51 checked by the check-source.py script.
53 A more detailed description can be found in the developer manual.
61 @functools.total_ordering
62 class IncludeGroup(object):
64 """Enumeration type for grouping includes."""
66 def __init__(self, value):
67 """Initialize a IncludeGroup instance.
69 IncludeGroup.{main,system_c,...} should be used outside the
70 class instead of calling the constructor.
74 def __eq__(self, other):
75 """Order include groups in the desired order."""
76 return self._value == other._value
78 def __lt__(self, other):
79 """Order include groups in the desired order."""
80 return self._value < other._value
82 # gmxpre.h is always first
83 IncludeGroup.pre = IncludeGroup(0)
84 # "main" include file for the source file is next
85 IncludeGroup.main = IncludeGroup(1)
86 # config.h is next, if present, to keep its location consistent
87 IncludeGroup.config = IncludeGroup(2)
88 # Followed by system headers, with C first and C++ following
89 IncludeGroup.system_c = IncludeGroup(3)
90 IncludeGroup.system_c_cpp = IncludeGroup(4)
91 IncludeGroup.system_cpp = IncludeGroup(5)
92 # System headers not in standard C/C++ are in a separate block
93 IncludeGroup.system_other = IncludeGroup(6)
94 # src/external/ contents that are included with quotes go here
95 IncludeGroup.nonsystem_other = IncludeGroup(7)
96 # Other GROMACS headers
97 IncludeGroup.gmx_general = IncludeGroup(8)
98 # This group is for shared (unit) testing utilities
99 IncludeGroup.gmx_test = IncludeGroup(9)
100 # This group is for headers local to the including file/module
101 IncludeGroup.gmx_local = IncludeGroup(10)
103 class GroupedSorter(object):
105 """Grouping and formatting logic for #include directives.
107 This class implements the actual logic that decides how includes are
108 grouped and sorted, and how they are formatted."""
110 # These variables contain the list of system headers for various blocks
111 _std_c_headers = ['assert.h', 'ctype.h', 'errno.h', 'float.h',
112 'inttypes.h', 'limits.h', 'math.h', 'signal.h', 'stdarg.h',
113 'stddef.h', 'stdint.h', 'stdio.h', 'stdlib.h', 'string.h',
115 _std_c_cpp_headers = ['c' + x[:-2] for x in _std_c_headers]
116 _std_cpp_headers = ['algorithm',
179 def __init__(self, style='pub-priv', absolute=False):
180 """Initialize a sorted with the given style."""
181 if style == 'single-group':
182 self._local_group = 'none'
183 elif style == 'pub-priv':
184 self._local_group = 'private'
186 self._local_group = 'local'
188 self._abspath_main = True
189 self._abspath_local = True
191 self._abspath_main = False
192 self._abspath_local = False
194 def _get_path(self, included_file, group, including_file):
195 """Compute include path to use for an #include.
197 The path is made either absolute (i.e., relative to src/), or
198 relative to the location of the including file, depending on the group
201 use_abspath = including_file is None or group is None
203 if group in (IncludeGroup.gmx_general, IncludeGroup.gmx_test):
205 elif group == IncludeGroup.main and self._abspath_main:
207 elif group == IncludeGroup.gmx_local and self._abspath_local:
210 fromdir = os.path.dirname(including_file.get_abspath())
211 relpath = os.path.relpath(included_file.get_abspath(), fromdir)
212 if not relpath.startswith('..'):
214 path = included_file.get_relpath()
215 assert path.startswith('src/')
218 def _get_gmx_group(self, including_file, included_file):
219 """Determine group for GROMACS headers.
221 Helper function to determine the group for an #include directive
222 when the #include is in one of the gmx_* groups (or in the main group).
224 main_header = including_file.get_main_header()
225 if main_header and main_header == included_file:
226 return IncludeGroup.main
227 if included_file.get_directory().get_name() == 'testutils':
228 return IncludeGroup.gmx_test
229 if including_file.get_directory().contains(included_file):
230 if self._local_group == 'local':
231 return IncludeGroup.gmx_local
232 if self._local_group == 'private':
233 if included_file.api_type_is_reliable() \
234 and included_file.is_module_internal():
235 return IncludeGroup.gmx_local
236 if not included_file.api_type_is_reliable() \
237 and including_file.get_relpath().startswith('src/programs'):
238 return IncludeGroup.gmx_local
239 if included_file.is_test_file():
240 return IncludeGroup.gmx_test
241 return IncludeGroup.gmx_general
243 def _split_path(self, path):
244 """Split include path into sortable compoments.
246 Plain string on the full path in the #include directive causes some
247 unintuitive behavior, so this splits the path into a tuple at
248 points that allow more natural sorting: primary sort criterion is the
249 directory name, followed by the basename (without extension) of the
252 path_components = list(os.path.split(path))
253 path_components[1] = os.path.splitext(path_components[1])
254 return tuple(path_components)
256 def _join_path(self, path_components):
257 """Reconstruct path from the return value of _split_path."""
258 return os.path.join(path_components[0], ''.join(path_components[1]))
260 def get_sortable_object(self, include):
261 """Produce a sortable, opaque object for an include.
263 Includes are sorted by calling this function for each #include object,
264 and sorting the list made up of these objects (using the default
265 comparison operators). Each element from the sorted list is then
266 passed to format_include(), which extracts information from the opaque
267 object and formats the #include directive for output.
269 included_file = include.get_file()
270 if not included_file:
271 path = include.get_included_path()
272 if path in self._std_c_headers:
273 group = IncludeGroup.system_c
274 elif path in self._std_c_cpp_headers:
275 group = IncludeGroup.system_c_cpp
276 elif path in self._std_cpp_headers:
277 group = IncludeGroup.system_cpp
279 group = IncludeGroup.system_other
280 elif included_file.is_external():
281 group = IncludeGroup.nonsystem_other
282 if 'external/' in include.get_included_path():
283 path = self._get_path(included_file, group, None)
285 path = include.get_included_path()
286 elif included_file.get_name() == 'gmxpre.h':
287 group = IncludeGroup.pre
288 path = self._get_path(included_file, group, None)
289 elif included_file.get_name() == 'config.h':
290 group = IncludeGroup.config
291 path = self._get_path(included_file, group, None)
293 including_file = include.get_including_file()
294 group = self._get_gmx_group(including_file, included_file)
295 path = self._get_path(included_file, group, including_file)
296 return (group, self._split_path(path), include)
298 def format_include(self, obj, prev):
299 """Format an #include directive after sorting."""
302 if prev[0] != obj[0]:
303 # Print empty line between groups
305 elif prev[1] == obj[1]:
309 line = include.get_full_line()
310 include_re = r'^(?P<head>\s*#\s*include\s+)["<][^">]*[">](?P<tail>.*)$'
311 match = re.match(include_re, line)
313 if include.is_system():
314 path = '<{0}>'.format(self._join_path(obj[1]))
316 path = '"{0}"'.format(self._join_path(obj[1]))
317 result.append('{0}{1}{2}\n'.format(match.group('head'), path, match.group('tail')))
320 class IncludeSorter(object):
322 """High-level logic for sorting includes.
324 This class contains the high-level logic for sorting include statements.
325 The actual ordering and formatting the includes is delegated to a sort method
326 (see GroupedSorter) to keep things separated.
329 def __init__(self, sortmethod=None, quiet=True):
330 """Initialize the include sorter with the given sorter and options."""
332 sortmethod = GroupedSorter()
333 self._sortmethod = sortmethod
335 self._changed = False
337 def _sort_include_block(self, block, lines):
338 """Sort a single include block.
340 Returns a new list of lines for the block.
341 If anything is changed, self._changed is set to True, and the caller
343 includes = sorted(map(self._sortmethod.get_sortable_object, block.get_includes()))
346 current_line_number = block.get_first_line()-1
347 for include in includes:
348 newlines = self._sortmethod.format_include(include, prev)
349 result.extend(newlines)
350 if not self._changed:
351 for offset, newline in enumerate(newlines):
352 if lines[current_line_number + offset] != newline:
355 current_line_number += len(newlines)
359 def sort_includes(self, fileobj):
360 """Sort all includes in a file."""
361 lines = fileobj.get_contents()
362 # Format into a list first:
363 # - avoid bugs or issues in the script truncating the file
364 # - can check whether anything was changed before touching the file
367 self._changed = False
368 for block in fileobj.get_include_blocks():
369 newlines.extend(lines[prev:block.get_first_line()-1])
370 newlines.extend(self._sort_include_block(block, lines))
371 # The returned values are 1-based, but indexing here is 0-based,
372 # so an explicit +1 is not needed.
373 prev = block.get_last_line()
376 sys.stderr.write('{0}: includes reformatted\n'.format(fileobj.get_relpath()))
377 newlines.extend(lines[prev:])
378 with open(fileobj.get_abspath(), 'w') as fp:
379 fp.write(''.join(newlines))
381 def check_sorted(self, fileobj):
382 """Check that includes within a file are sorted."""
383 # TODO: Make the checking work without full contents of the file
384 lines = fileobj.get_contents()
387 for block in fileobj.get_include_blocks():
388 self._changed = False
389 sorted_lines = self._sort_include_block(block, lines)
392 # TODO: Do a proper diff to show the actual changes.
394 details = ["Correct order/style is:"]
396 details.append(" ...")
397 details.extend([" " + x.rstrip() for x in sorted_lines])
398 return (is_sorted, details)
401 """Run the include sorter script."""
405 from optparse import OptionParser
407 from gmxtree import GromacsTree
408 from reporter import Reporter
410 parser = OptionParser()
411 parser.add_option('-S', '--source-root',
412 help='Source tree root directory')
413 parser.add_option('-B', '--build-root',
414 help='Build tree root directory')
415 parser.add_option('-F', '--files',
416 help='Specify files to sort')
417 parser.add_option('-q', '--quiet', action='store_true',
418 help='Do not write status messages')
419 # This is for evaluating different options; can be removed from the final
421 parser.add_option('-s', '--style', type='choice', default='pub-priv',
422 choices=('single-group', 'pub-priv', 'pub-local'),
423 help='Style for GROMACS includes')
424 parser.add_option('--absolute', action='store_true',
425 help='Write all include paths relative to src/')
426 options, args = parser.parse_args()
430 if options.files == '-':
431 lines = sys.stdin.readlines()
433 with open(options.files, 'r') as fp:
434 lines = fp.readlines()
435 filelist.extend([x.strip() for x in lines])
437 reporter = Reporter(quiet=True)
439 if not options.quiet:
440 sys.stderr.write('Scanning source tree...\n')
441 if not options.source_root:
442 sys.stderr.write('Source root required not specified.\n')
444 if not options.build_root:
445 sys.stderr.write('Build root required not specified.\n')
447 tree = GromacsTree(options.source_root, options.build_root, reporter)
449 for filename in filelist:
450 fileobj = tree.get_file(os.path.abspath(filename))
452 sys.stderr.write('warning: ignoring unknown file {0}\n'.format(filename))
454 files.append(fileobj)
455 if not options.quiet:
456 sys.stderr.write('Reading source files...\n')
457 tree.scan_files(only_files=files, keep_contents=True)
458 extfiles = set(files)
459 for fileobj in files:
460 for included_file in fileobj.get_includes():
461 other_file = included_file.get_file()
463 extfiles.add(other_file)
464 if not options.quiet:
465 sys.stderr.write('Reading Doxygen XML files...\n')
466 tree.load_xml(only_files=extfiles)
468 if not options.quiet:
469 sys.stderr.write('Sorting includes...\n')
471 sorter = IncludeSorter(GroupedSorter(options.style, options.absolute), options.quiet)
473 for fileobj in files:
474 sorter.sort_includes(fileobj)
476 if __name__ == '__main__':