check-includes.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. #! /usr/bin/env python
  2. """
  3. check-includes.py <file...>
  4. Checks if the includes are sorted properly and following the "system headers
  5. before local headers" rule.
  6. Ignores what is in #if blocks to avoid false negatives.
  7. """
  8. import re
  9. import sys
  10. def exclude_if_blocks(lines):
  11. '''Removes lines from #if ... #endif blocks.'''
  12. level = 0
  13. for l in lines:
  14. if l.startswith('#if'):
  15. level += 1
  16. elif l.startswith('#endif'):
  17. level -= 1
  18. elif level == 0:
  19. yield l
  20. def filter_includes(lines):
  21. '''Removes lines that are not #include and keeps only the file part.'''
  22. for l in lines:
  23. if l.startswith('#include'):
  24. if 'NOLINT' not in l:
  25. yield l.split(' ')[1]
  26. class IncludeFileSorter(object):
  27. def __init__(self, path):
  28. self.path = path
  29. def __lt__(self, other):
  30. '''Sorting function for include files.
  31. * System headers go before local headers (check the first character -
  32. if it's different, then the one starting with " is the 'larger').
  33. * Then, iterate on all the path components:
  34. * If they are equal, try to continue to the next path component.
  35. * If not, return whether the path component are smaller/larger.
  36. * Paths with less components should go first, so after iterating, check
  37. whether one path still has some / in it.
  38. '''
  39. a, b = self.path, other.path
  40. if a[0] != b[0]:
  41. return False if a[0] == '"' else True
  42. a, b = a[1:-1].lower(), b[1:-1].lower()
  43. while '/' in a and '/' in b:
  44. ca, a = a.split('/', 1)
  45. cb, b = b.split('/', 1)
  46. if ca != cb:
  47. return ca < cb
  48. if '/' in a:
  49. return False
  50. elif '/' in b:
  51. return True
  52. else:
  53. return a < b
  54. def __eq__(self, other):
  55. return self.path.lower() == other.path.lower()
  56. def sort_includes(includes):
  57. return sorted(includes, key=IncludeFileSorter)
  58. def show_differences(bad, good):
  59. bad = [' Current'] + bad
  60. good = [' Should be'] + good
  61. longest = max(len(i) for i in bad)
  62. padded = [i + ' ' * (longest + 4 - len(i)) for i in bad]
  63. return '\n'.join('%s%s' % t for t in zip(padded, good))
  64. def check_file(path):
  65. print('Checking %s' % path)
  66. try:
  67. try:
  68. data = open(path, encoding='utf-8').read()
  69. except TypeError: # py2
  70. data = open(path).read().decode('utf-8')
  71. except UnicodeDecodeError:
  72. sys.stderr.write('%s: bad UTF-8 data\n' % path)
  73. return
  74. lines = (l.strip() for l in data.split('\n'))
  75. lines = exclude_if_blocks(lines)
  76. includes = list(filter_includes(lines))
  77. sorted_includes = sort_includes(includes)
  78. if includes != sorted_includes:
  79. sys.stderr.write('%s: includes are incorrect\n' % path)
  80. sys.stderr.write(show_differences(includes, sorted_includes) + '\n')
  81. if __name__ == '__main__':
  82. for path in sys.argv[1:]:
  83. check_file(path)