check_lint.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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 subprocess
  22. import sys
  23. import textwrap
  24. from lib import checker
  25. from lib import command_trace
  26. from lib import git
  27. from lib import source
  28. _logger = logging.getLogger('lint')
  29. _dry_run = False
  30. _CPPLINT_OBJC_FILTERS = [
  31. # Objective-C uses #import and does not use header guards
  32. '-build/header_guard',
  33. # Inline definitions of Objective-C blocks confuse
  34. '-readability/braces',
  35. # C-style casts are acceptable in Objective-C++
  36. '-readability/casting',
  37. # Objective-C needs use type 'long' for interop between types like NSInteger
  38. # and printf-style functions.
  39. '-runtime/int',
  40. # cpplint is generally confused by Objective-C mixing with C++.
  41. # * Objective-C method invocations in a for loop make it think its a
  42. # range-for
  43. # * Objective-C dictionary literals confuse brace spacing
  44. # * Empty category declarations ("@interface Foo ()") look like function
  45. # invocations
  46. '-whitespace',
  47. ]
  48. _CPPLINT_OBJC_OPTIONS = [
  49. # cpplint normally excludes Objective-C++
  50. '--extensions=h,m,mm',
  51. # Objective-C style allows longer lines
  52. '--linelength=100',
  53. '--filter=' + ','.join(_CPPLINT_OBJC_FILTERS),
  54. ]
  55. def main():
  56. global _dry_run
  57. parser = argparse.ArgumentParser(description='Lint source files.')
  58. parser.add_argument('--dry-run', '-n', action='store_true',
  59. help='Show what the linter would do without doing it')
  60. parser.add_argument('--all', action='store_true',
  61. help='run the linter over all known sources')
  62. parser.add_argument('rev_or_files', nargs='*',
  63. help='A single revision that specifies a point in time '
  64. 'from which to look for changes. Defaults to '
  65. 'origin/master. Alternatively, a list of specific '
  66. 'files or git pathspecs to lint.')
  67. args = command_trace.parse_args(parser)
  68. if args.dry_run:
  69. _dry_run = True
  70. command_trace.enable_tracing()
  71. pool = checker.Pool()
  72. sources = _unique(source.CC_DIRS + source.OBJC_DIRS + source.PYTHON_DIRS)
  73. patterns = git.make_patterns(sources)
  74. files = git.find_changed_or_files(args.all, args.rev_or_files, patterns)
  75. check(pool, files)
  76. pool.exit()
  77. def check(pool, files):
  78. group = source.categorize_files(files)
  79. for kind, files in group.kinds.items():
  80. for chunk in checker.shard(files):
  81. if not chunk:
  82. continue
  83. linter = _linters[kind]
  84. pool.submit(linter, chunk)
  85. def lint_cc(files):
  86. return _run_cpplint([], files)
  87. def lint_objc(files):
  88. return _run_cpplint(_CPPLINT_OBJC_OPTIONS, files)
  89. def _run_cpplint(options, files):
  90. scripts_dir = os.path.dirname(os.path.abspath(__file__))
  91. cpplint = os.path.join(scripts_dir, 'cpplint.py')
  92. command = [sys.executable, cpplint, '--quiet']
  93. command.extend(options)
  94. command.extend(files)
  95. return _read_output(command)
  96. _flake8_warned = False
  97. def lint_py(files):
  98. flake8 = which('flake8')
  99. if flake8 is None:
  100. global _flake8_warned
  101. if not _flake8_warned:
  102. _flake8_warned = True
  103. _logger.warn(textwrap.dedent(
  104. """
  105. Could not find flake8 on $PATH; skipping python lint.
  106. Install with:
  107. pip install --user flake8
  108. """))
  109. return
  110. command = [flake8]
  111. command.extend(files)
  112. return _read_output(command)
  113. def _read_output(command):
  114. command_trace.log(command)
  115. if _dry_run:
  116. return checker.Result(0, '')
  117. proc = subprocess.Popen(
  118. command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  119. output = proc.communicate('')[0]
  120. sc = proc.wait()
  121. return checker.Result(sc, output)
  122. _linters = {
  123. 'cc': lint_cc,
  124. 'objc': lint_objc,
  125. 'py': lint_py,
  126. }
  127. def _unique(items):
  128. return list(set(items))
  129. def which(executable):
  130. """Finds the executable with the given name.
  131. Returns:
  132. The fully qualified path to the executable or None if the executable isn't
  133. found.
  134. """
  135. if executable.startswith('/'):
  136. return executable
  137. path = os.environ['PATH'].split(os.pathsep)
  138. for executable_with_ext in _executable_names(executable):
  139. for entry in path:
  140. joined = os.path.join(entry, executable_with_ext)
  141. if os.path.isfile(joined) and os.access(joined, os.X_OK):
  142. return joined
  143. return None
  144. def _executable_names(executable):
  145. """Yields a sequence of all possible executable names."""
  146. if os.name == 'nt':
  147. pathext = os.environ.get('PATHEXT', '').split(os.pathsep)
  148. for ext in pathext:
  149. yield executable + ext
  150. else:
  151. yield executable
  152. if __name__ == '__main__':
  153. main()