3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2012,2013,2014,2015,2016, 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 list of installed headers and 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.
57 class IncludeGroup(object):
59 """Enumeration type for grouping includes."""
61 def __init__(self, value):
62 """Initialize a IncludeGroup instance.
64 IncludeGroup.{main,system_c,...} should be used outside the
65 class instead of calling the constructor.
69 def __cmp__(self, other):
70 """Order include groups in the desired order."""
71 return cmp(self._value, other._value)
73 # gmxpre.h is always first
74 IncludeGroup.pre = IncludeGroup(0)
75 # "main" include file for the source file is next
76 IncludeGroup.main = IncludeGroup(1)
77 # config.h is next, if present, to keep its location consistent
78 IncludeGroup.config = IncludeGroup(2)
79 # Followed by system headers, with C first and C++ following
80 IncludeGroup.system_c = IncludeGroup(3)
81 IncludeGroup.system_c_cpp = IncludeGroup(4)
82 IncludeGroup.system_cpp = IncludeGroup(5)
83 # System headers not in standard C/C++ are in a separate block
84 IncludeGroup.system_other = IncludeGroup(6)
85 # src/external/ contents that are included with quotes go here
86 IncludeGroup.nonsystem_other = IncludeGroup(7)
87 # Other GROMACS headers
88 IncludeGroup.gmx_general = IncludeGroup(8)
89 # This group is for shared (unit) testing utilities
90 IncludeGroup.gmx_test = IncludeGroup(9)
91 # This group is for headers local to the including file/module
92 IncludeGroup.gmx_local = IncludeGroup(10)
94 class GroupedSorter(object):
96 """Grouping and formatting logic for #include directives.
98 This class implements the actual logic that decides how includes are
99 grouped and sorted, and how they are formatted."""
101 # These variables contain the list of system headers for various blocks
102 _std_c_headers = ['assert.h', 'ctype.h', 'errno.h', 'float.h',
103 'inttypes.h', 'limits.h', 'math.h', 'signal.h', 'stdarg.h',
104 'stddef.h', 'stdint.h', 'stdio.h', 'stdlib.h', 'string.h',
106 _std_c_cpp_headers = ['c' + x[:-2] for x in _std_c_headers]
107 _std_cpp_headers = ['algorithm', 'array', 'chrono', 'deque', 'exception', 'fstream',
108 'functional', 'initializer_list', 'iomanip', 'ios', 'iosfwd',
109 'iostream', 'istream', 'iterator',
110 'limits', 'list', 'map', 'memory', 'new', 'numeric', 'ostream', 'random',
111 'regex', 'set', 'sstream', 'stdexcept', 'streambuf', 'string', 'strstream',
112 'thread', 'tuple', 'type_traits', 'typeindex', 'typeinfo', 'vector', 'utility']
114 def __init__(self, style='pub-priv', absolute=False):
115 """Initialize a sorted with the given style."""
116 if style == 'single-group':
117 self._local_group = 'none'
118 elif style == 'pub-priv':
119 self._local_group = 'private'
121 self._local_group = 'local'
123 self._abspath_main = True
124 self._abspath_local = True
126 self._abspath_main = False
127 self._abspath_local = False
129 def _get_path(self, included_file, group, including_file):
130 """Compute include path to use for an #include.
132 The path is made either absolute (i.e., relative to src/), or
133 relative to the location of the including file, depending on the group
136 use_abspath = including_file is None or group is None
138 if group in (IncludeGroup.gmx_general, IncludeGroup.gmx_test):
140 elif group == IncludeGroup.main and self._abspath_main:
142 elif group == IncludeGroup.gmx_local and self._abspath_local:
145 fromdir = os.path.dirname(including_file.get_abspath())
146 relpath = os.path.relpath(included_file.get_abspath(), fromdir)
147 if not relpath.startswith('..'):
149 path = included_file.get_relpath()
150 assert path.startswith('src/')
153 def _get_gmx_group(self, including_file, included_file):
154 """Determine group for GROMACS headers.
156 Helper function to determine the group for an #include directive
157 when the #include is in one of the gmx_* groups (or in the main group).
159 main_header = including_file.get_main_header()
160 if main_header and main_header == included_file:
161 return IncludeGroup.main
162 if included_file.get_directory().get_name() == 'testutils':
163 return IncludeGroup.gmx_test
164 if including_file.get_directory().contains(included_file):
165 if self._local_group == 'local':
166 return IncludeGroup.gmx_local
167 if self._local_group == 'private':
168 if included_file.api_type_is_reliable() \
169 and included_file.is_module_internal():
170 return IncludeGroup.gmx_local
171 if not included_file.api_type_is_reliable() \
172 and including_file.get_relpath().startswith('src/programs'):
173 return IncludeGroup.gmx_local
174 if included_file.is_test_file():
175 return IncludeGroup.gmx_test
176 return IncludeGroup.gmx_general
178 def _split_path(self, path):
179 """Split include path into sortable compoments.
181 Plain string on the full path in the #include directive causes some
182 unintuitive behavior, so this splits the path into a tuple at
183 points that allow more natural sorting: primary sort criterion is the
184 directory name, followed by the basename (without extension) of the
187 path_components = list(os.path.split(path))
188 path_components[1] = os.path.splitext(path_components[1])
189 return tuple(path_components)
191 def _join_path(self, path_components):
192 """Reconstruct path from the return value of _split_path."""
193 return os.path.join(path_components[0], ''.join(path_components[1]))
195 def get_sortable_object(self, include):
196 """Produce a sortable, opaque object for an include.
198 Includes are sorted by calling this function for each #include object,
199 and sorting the list made up of these objects (using the default
200 comparison operators). Each element from the sorted list is then
201 passed to format_include(), which extracts information from the opaque
202 object and formats the #include directive for output.
204 included_file = include.get_file()
205 if not included_file:
206 path = include.get_included_path()
207 if path in self._std_c_headers:
208 group = IncludeGroup.system_c
209 elif path in self._std_c_cpp_headers:
210 group = IncludeGroup.system_c_cpp
211 elif path in self._std_cpp_headers:
212 group = IncludeGroup.system_cpp
214 group = IncludeGroup.system_other
215 elif included_file.is_external():
216 group = IncludeGroup.nonsystem_other
217 if 'external/' in include.get_included_path():
218 path = self._get_path(included_file, group, None)
220 path = include.get_included_path()
221 elif included_file.get_name() == 'gmxpre.h':
222 group = IncludeGroup.pre
223 path = self._get_path(included_file, group, None)
224 elif included_file.get_name() == 'config.h':
225 group = IncludeGroup.config
226 path = self._get_path(included_file, group, None)
228 including_file = include.get_including_file()
229 group = self._get_gmx_group(including_file, included_file)
230 path = self._get_path(included_file, group, including_file)
231 return (group, self._split_path(path), include)
233 def format_include(self, obj, prev):
234 """Format an #include directive after sorting."""
237 if prev[0] != obj[0]:
238 # Print empty line between groups
240 elif prev[1] == obj[1]:
244 line = include.get_full_line()
245 include_re = r'^(?P<head>\s*#\s*include\s+)["<][^">]*[">](?P<tail>.*)$'
246 match = re.match(include_re, line)
248 if include.is_system():
249 path = '<{0}>'.format(self._join_path(obj[1]))
251 path = '"{0}"'.format(self._join_path(obj[1]))
252 result.append('{0}{1}{2}\n'.format(match.group('head'), path, match.group('tail')))
255 class IncludeSorter(object):
257 """High-level logic for sorting includes.
259 This class contains the high-level logic for sorting include statements.
260 The actual ordering and formatting the includes is delegated to a sort method
261 (see GroupedSorter) to keep things separated.
264 def __init__(self, sortmethod=None, quiet=True):
265 """Initialize the include sorter with the given sorter and options."""
267 sortmethod = GroupedSorter()
268 self._sortmethod = sortmethod
270 self._changed = False
272 def _sort_include_block(self, block, lines):
273 """Sort a single include block.
275 Returns a new list of lines for the block.
276 If anything is changed, self._changed is set to True, and the caller
278 includes = map(self._sortmethod.get_sortable_object, block.get_includes())
282 current_line_number = block.get_first_line()-1
283 for include in includes:
284 newlines = self._sortmethod.format_include(include, prev)
285 result.extend(newlines)
286 if not self._changed:
287 for offset, newline in enumerate(newlines):
288 if lines[current_line_number + offset] != newline:
291 current_line_number += len(newlines)
295 def sort_includes(self, fileobj):
296 """Sort all includes in a file."""
297 lines = fileobj.get_contents()
298 # Format into a list first:
299 # - avoid bugs or issues in the script truncating the file
300 # - can check whether anything was changed before touching the file
303 self._changed = False
304 for block in fileobj.get_include_blocks():
305 newlines.extend(lines[prev:block.get_first_line()-1])
306 newlines.extend(self._sort_include_block(block, lines))
307 # The returned values are 1-based, but indexing here is 0-based,
308 # so an explicit +1 is not needed.
309 prev = block.get_last_line()
312 sys.stderr.write('{0}: includes reformatted\n'.format(fileobj.get_relpath()))
313 newlines.extend(lines[prev:])
314 with open(fileobj.get_abspath(), 'w') as fp:
315 fp.write(''.join(newlines))
317 def check_sorted(self, fileobj):
318 """Check that includes within a file are sorted."""
319 # TODO: Make the checking work without full contents of the file
320 lines = fileobj.get_contents()
323 for block in fileobj.get_include_blocks():
324 self._changed = False
325 sorted_lines = self._sort_include_block(block, lines)
328 # TODO: Do a proper diff to show the actual changes.
330 details = ["Correct order/style is:"]
332 details.append(" ...")
333 details.extend([" " + x.rstrip() for x in sorted_lines])
334 return (is_sorted, details)
337 """Run the include sorter script."""
341 from optparse import OptionParser
343 from gmxtree import GromacsTree
344 from reporter import Reporter
346 parser = OptionParser()
347 parser.add_option('-S', '--source-root',
348 help='Source tree root directory')
349 parser.add_option('-B', '--build-root',
350 help='Build tree root directory')
351 parser.add_option('-F', '--files',
352 help='Specify files to sort')
353 parser.add_option('-q', '--quiet', action='store_true',
354 help='Do not write status messages')
355 # This is for evaluating different options; can be removed from the final
357 parser.add_option('-s', '--style', type='choice', default='pub-priv',
358 choices=('single-group', 'pub-priv', 'pub-local'),
359 help='Style for GROMACS includes')
360 parser.add_option('--absolute', action='store_true',
361 help='Write all include paths relative to src/')
362 options, args = parser.parse_args()
366 if options.files == '-':
367 lines = sys.stdin.readlines()
369 with open(options.files, 'r') as fp:
370 lines = fp.readlines()
371 filelist.extend([x.strip() for x in lines])
373 reporter = Reporter(quiet=True)
375 if not options.quiet:
376 sys.stderr.write('Scanning source tree...\n')
377 if not options.source_root:
378 sys.stderr.write('Source root required not specified.\n')
380 if not options.build_root:
381 sys.stderr.write('Build root required not specified.\n')
383 tree = GromacsTree(options.source_root, options.build_root, reporter)
384 tree.load_installed_file_list()
386 for filename in filelist:
387 fileobj = tree.get_file(os.path.abspath(filename))
389 sys.stderr.write('warning: ignoring unknown file {0}\n'.format(filename))
391 files.append(fileobj)
392 if not options.quiet:
393 sys.stderr.write('Reading source files...\n')
394 tree.scan_files(only_files=files, keep_contents=True)
395 extfiles = set(files)
396 for fileobj in files:
397 for included_file in fileobj.get_includes():
398 other_file = included_file.get_file()
400 extfiles.add(other_file)
401 if not options.quiet:
402 sys.stderr.write('Reading Doxygen XML files...\n')
403 tree.load_xml(only_files=extfiles)
405 if not options.quiet:
406 sys.stderr.write('Sorting includes...\n')
408 sorter = IncludeSorter(GroupedSorter(options.style, options.absolute), options.quiet)
410 for fileobj in files:
411 sorter.sort_includes(fileobj)
413 if __name__ == '__main__':