check_lint.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. #!/usr/bin/env python
  2. # Copyright 2019 Google
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Lints source files for conformance with the style guide that applies.
  16. Currently supports linting Objective-C, Objective-C++, C++, and Python source.
  17. """
  18. import argparse
  19. import logging
  20. import os
  21. import re
  22. import subprocess
  23. import sys
  24. import textwrap
  25. from lib import checker
  26. from lib import command_trace
  27. from lib import git
  28. from lib import source
  29. _logger = logging.getLogger('lint')
  30. _dry_run = False
  31. _CPPLINT_OBJC_FILTERS = [
  32. # Objective-C uses #import and does not use header guards
  33. '-build/header_guard',
  34. # Inline definitions of Objective-C blocks confuse
  35. '-readability/braces',
  36. # C-style casts are acceptable in Objective-C++
  37. '-readability/casting',
  38. # Objective-C needs use type 'long' for interop between types like NSInteger
  39. # and printf-style functions.
  40. '-runtime/int',
  41. # cpplint is generally confused by Objective-C mixing with C++.
  42. # * Objective-C method invocations in a for loop make it think its a
  43. # range-for
  44. # * Objective-C dictionary literals confuse brace spacing
  45. # * Empty category declarations ("@interface Foo ()") look like function
  46. # invocations
  47. '-whitespace',
  48. ]
  49. _CPPLINT_OBJC_OPTIONS = [
  50. # cpplint normally excludes Objective-C++
  51. '--extensions=h,m,mm',
  52. # Objective-C style allows longer lines
  53. '--linelength=100',
  54. '--filter=' + ','.join(_CPPLINT_OBJC_FILTERS),
  55. ]
  56. def main():
  57. global _dry_run
  58. parser = argparse.ArgumentParser(description='Lint source files.')
  59. parser.add_argument('--dry-run', '-n', action='store_true',
  60. help='Show what the linter would do without doing it')
  61. parser.add_argument('--all', action='store_true',
  62. help='run the linter over all known sources')
  63. parser.add_argument('rev_or_files', nargs='*',
  64. help='A single revision that specifies a point in time '
  65. 'from which to look for changes. Defaults to '
  66. 'origin/master. Alternatively, a list of specific '
  67. 'files or git pathspecs to lint.')
  68. args = command_trace.parse_args(parser)
  69. if args.dry_run:
  70. _dry_run = True
  71. command_trace.enable_tracing()
  72. pool = checker.Pool()
  73. sources = _unique(source.CC_DIRS + source.OBJC_DIRS + source.PYTHON_DIRS)
  74. patterns = git.make_patterns(sources)
  75. files = git.find_changed_or_files(args.all, args.rev_or_files, patterns)
  76. check(pool, files)
  77. pool.exit()
  78. def check(pool, files):
  79. group = source.categorize_files(files)
  80. for kind, files in group.kinds.items():
  81. for chunk in checker.shard(files):
  82. if not chunk:
  83. continue
  84. linter = _linters[kind]
  85. pool.submit(linter, chunk)
  86. def lint_cc(files):
  87. return _run_cpplint([], files)
  88. def lint_objc(files):
  89. return _run_cpplint(_CPPLINT_OBJC_OPTIONS, files)
  90. def _run_cpplint(options, files):
  91. scripts_dir = os.path.dirname(os.path.abspath(__file__))
  92. cpplint = os.path.join(scripts_dir, 'cpplint.py')
  93. command = [sys.executable, cpplint, '--quiet']
  94. command.extend(options)
  95. command.extend(files)
  96. return _read_output(command)
  97. _flake8_warned = False
  98. def lint_py(files):
  99. flake8 = which('flake8')
  100. if flake8 is None:
  101. global _flake8_warned
  102. if not _flake8_warned:
  103. _flake8_warned = True
  104. _logger.warn(textwrap.dedent(
  105. """
  106. Could not find flake8 on the path; skipping python lint.
  107. Install with:
  108. pip install --user flake8
  109. """))
  110. return
  111. command = [flake8]
  112. command.extend(files)
  113. return _read_output(command)
  114. def _read_output(command):
  115. command_trace.log(command)
  116. if _dry_run:
  117. return checker.Result(0, '')
  118. proc = subprocess.Popen(
  119. command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  120. output = proc.communicate('')[0]
  121. sc = proc.wait()
  122. return checker.Result(sc, output)
  123. _linters = {
  124. 'cc': lint_cc,
  125. 'objc': lint_objc,
  126. 'py': lint_py,
  127. }
  128. def _unique(items):
  129. return list(set(items))
  130. def make_path():
  131. """Makes a list of paths to search for binaries.
  132. Returns:
  133. A list of directories that can be sources of binaries to run. This includes
  134. both the PATH environment variable and any bin directories associated with
  135. python install locations.
  136. """
  137. # Start with the system-supplied PATH.
  138. path = os.environ['PATH'].split(os.pathsep)
  139. # In addition, add any bin directories near the lib directories in the python
  140. # path. This makes it possible to find flake8 in ~/Library/Python/2.7/bin
  141. # after pip install --user flake8. Also handle installations on Windows which
  142. # go in %APPDATA%/Python/Scripts.
  143. lib_pattern = re.compile(r'(.*)/[^/]*/site-packages')
  144. for entry in sys.path:
  145. entry = entry.replace(os.sep, '/')
  146. m = lib_pattern.match(entry)
  147. if not m:
  148. continue
  149. python_root = m.group(1).replace('/', os.sep)
  150. for bin_basename in ('bin', 'Scripts'):
  151. bin_dir = os.path.join(python_root, bin_basename)
  152. if bin_dir not in path and os.path.exists(bin_dir):
  153. path.append(bin_dir)
  154. return path
  155. _PATH = make_path()
  156. def which(executable):
  157. """Finds the executable with the given name.
  158. Returns:
  159. The fully qualified path to the executable or None if the executable isn't
  160. found.
  161. """
  162. if executable.startswith('/'):
  163. return executable
  164. for executable_with_ext in _executable_names(executable):
  165. for entry in _PATH:
  166. joined = os.path.join(entry, executable_with_ext)
  167. if os.path.isfile(joined) and os.access(joined, os.X_OK):
  168. return joined
  169. return None
  170. def _executable_names(executable):
  171. """Yields a sequence of all possible executable names."""
  172. if os.name == 'nt':
  173. pathext = os.environ.get('PATHEXT', '').split(os.pathsep)
  174. for ext in pathext:
  175. yield executable + ext
  176. else:
  177. yield executable
  178. if __name__ == '__main__':
  179. main()