Merge branch release-2016
[alexxy/gromacs.git] / docs / doxygen / includesorter.py
1 #!/usr/bin/python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
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.
9 #
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.
14 #
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.
19 #
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.
24 #
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.
32 #
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.
35
36 """Include directive sorter for GROMACS.
37
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.
47
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.
51 """
52
53 import os.path
54 import re
55 import sys
56
57 class IncludeGroup(object):
58
59     """Enumeration type for grouping includes."""
60
61     def __init__(self, value):
62         """Initialize a IncludeGroup instance.
63
64         IncludeGroup.{main,system_c,...} should be used outside the
65         class instead of calling the constructor.
66         """
67         self._value = value
68
69     def __cmp__(self, other):
70         """Order include groups in the desired order."""
71         return cmp(self._value, other._value)
72
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)
93
94 class GroupedSorter(object):
95
96     """Grouping and formatting logic for #include directives.
97
98     This class implements the actual logic that decides how includes are
99     grouped and sorted, and how they are formatted."""
100
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',
105             'time.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']
113
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'
120         else:
121             self._local_group = 'local'
122         if absolute:
123             self._abspath_main = True
124             self._abspath_local = True
125         else:
126             self._abspath_main = False
127             self._abspath_local = False
128
129     def _get_path(self, included_file, group, including_file):
130         """Compute include path to use for an #include.
131
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
134         the file is in.
135         """
136         use_abspath = including_file is None or group is None
137         if not use_abspath:
138             if group in (IncludeGroup.gmx_general, IncludeGroup.gmx_test):
139                 use_abspath = True
140             elif group == IncludeGroup.main and self._abspath_main:
141                 use_abspath = True
142             elif group == IncludeGroup.gmx_local and self._abspath_local:
143                 use_abspath = True
144         if not use_abspath:
145             fromdir = os.path.dirname(including_file.get_abspath())
146             relpath = os.path.relpath(included_file.get_abspath(), fromdir)
147             if not relpath.startswith('..'):
148                 return relpath
149         path = included_file.get_relpath()
150         assert path.startswith('src/')
151         return path[4:]
152
153     def _get_gmx_group(self, including_file, included_file):
154         """Determine group for GROMACS headers.
155
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).
158         """
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
177
178     def _split_path(self, path):
179         """Split include path into sortable compoments.
180
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
185         included file.
186         """
187         path_components = list(os.path.split(path))
188         path_components[1] = os.path.splitext(path_components[1])
189         return tuple(path_components)
190
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]))
194
195     def get_sortable_object(self, include):
196         """Produce a sortable, opaque object for an include.
197
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.
203         """
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
213             else:
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)
219             else:
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)
227         else:
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)
232
233     def format_include(self, obj, prev):
234         """Format an #include directive after sorting."""
235         result = []
236         if prev:
237             if prev[0] != obj[0]:
238                 # Print empty line between groups
239                 result.append('\n')
240             elif prev[1] == obj[1]:
241                 # Skip duplicates
242                 return result
243         include = obj[2]
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)
247         assert match
248         if include.is_system():
249             path = '<{0}>'.format(self._join_path(obj[1]))
250         else:
251             path = '"{0}"'.format(self._join_path(obj[1]))
252         result.append('{0}{1}{2}\n'.format(match.group('head'), path, match.group('tail')))
253         return result
254
255 class IncludeSorter(object):
256
257     """High-level logic for sorting includes.
258
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.
262     """
263
264     def __init__(self, sortmethod=None, quiet=True):
265         """Initialize the include sorter with the given sorter and options."""
266         if not sortmethod:
267             sortmethod = GroupedSorter()
268         self._sortmethod = sortmethod
269         self._quiet = quiet
270         self._changed = False
271
272     def _sort_include_block(self, block, lines):
273         """Sort a single include block.
274
275         Returns a new list of lines for the block.
276         If anything is changed, self._changed is set to True, and the caller
277         can check that."""
278         includes = map(self._sortmethod.get_sortable_object, block.get_includes())
279         includes.sort()
280         result = []
281         prev = None
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:
289                         self._changed = True
290                         break
291                 current_line_number += len(newlines)
292             prev = include
293         return result
294
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
301         newlines = []
302         prev = 0
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()
310         if self._changed:
311             if not self._quiet:
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))
316
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()
321         is_sorted = True
322         details = None
323         for block in fileobj.get_include_blocks():
324             self._changed = False
325             sorted_lines = self._sort_include_block(block, lines)
326             if self._changed:
327                 is_sorted = False
328                 # TODO: Do a proper diff to show the actual changes.
329                 if details is None:
330                     details = ["Correct order/style is:"]
331                 else:
332                     details.append("    ...")
333                 details.extend(["    " + x.rstrip() for x in sorted_lines])
334         return (is_sorted, details)
335
336 def main():
337     """Run the include sorter script."""
338     import os
339     import sys
340
341     from optparse import OptionParser
342
343     from gmxtree import GromacsTree
344     from reporter import Reporter
345
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
356     # version.
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()
363
364     filelist = args
365     if options.files:
366         if options.files == '-':
367             lines = sys.stdin.readlines()
368         else:
369             with open(options.files, 'r') as fp:
370                 lines = fp.readlines()
371         filelist.extend([x.strip() for x in lines])
372
373     reporter = Reporter(quiet=True)
374
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')
379         sys.exit(2)
380     if not options.build_root:
381         sys.stderr.write('Build root required not specified.\n')
382         sys.exit(2)
383     tree = GromacsTree(options.source_root, options.build_root, reporter)
384     tree.load_installed_file_list()
385     files = []
386     for filename in filelist:
387         fileobj = tree.get_file(os.path.abspath(filename))
388         if not fileobj:
389             sys.stderr.write('warning: ignoring unknown file {0}\n'.format(filename))
390             continue
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()
399             if other_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)
404
405     if not options.quiet:
406         sys.stderr.write('Sorting includes...\n')
407
408     sorter = IncludeSorter(GroupedSorter(options.style, options.absolute), options.quiet)
409
410     for fileobj in files:
411         sorter.sort_includes(fileobj)
412
413 if __name__ == '__main__':
414     main()