PageRenderTime 45ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/contrib/python/src/python/pants/contrib/python/checks/tasks/checkstyle/checker.py

https://gitlab.com/Ivy001/pants
Python | 191 lines | 120 code | 39 blank | 32 comment | 33 complexity | f43fbf4e974d6929e2e29e3ca05a75e5 MD5 | raw file
  1. # coding=utf-8
  2. # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
  3. # Licensed under the Apache License, Version 2.0 (see LICENSE).
  4. from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
  5. unicode_literals, with_statement)
  6. import re
  7. from collections import namedtuple
  8. from pants.backend.python.targets.python_target import PythonTarget
  9. from pants.backend.python.tasks.python_task import PythonTask
  10. from pants.base.build_environment import get_buildroot
  11. from pants.base.exceptions import TaskError
  12. from pants.option.custom_types import file_option
  13. from pants.contrib.python.checks.tasks.checkstyle.common import CheckSyntaxError, Nit, PythonFile
  14. from pants.contrib.python.checks.tasks.checkstyle.file_excluder import FileExcluder
  15. from pants.contrib.python.checks.tasks.checkstyle.register_plugins import register_plugins
  16. _NOQA_LINE_SEARCH = re.compile(r'# noqa\b').search
  17. _NOQA_FILE_SEARCH = re.compile(r'# (flake8|checkstyle): noqa$').search
  18. class LintPlugin(namedtuple('_LintPlugin', ['name', 'subsystem'])):
  19. def skip(self):
  20. return self.subsystem.global_instance().get_options().skip
  21. def checker(self, python_file):
  22. return self.subsystem.global_instance().get_plugin(python_file)
  23. def line_contains_noqa(line):
  24. return _NOQA_LINE_SEARCH(line) is not None
  25. def noqa_file_filter(python_file):
  26. return any(_NOQA_FILE_SEARCH(line) is not None for line in python_file.lines)
  27. class PythonCheckStyleTask(PythonTask):
  28. _PYTHON_SOURCE_EXTENSION = '.py'
  29. _plugins = []
  30. _subsystems = tuple()
  31. def __init__(self, *args, **kwargs):
  32. super(PythonCheckStyleTask, self).__init__(*args, **kwargs)
  33. self._plugins = [plugin for plugin in self._plugins if not plugin.skip()]
  34. self.options = self.get_options()
  35. self.excluder = FileExcluder(self.options.suppress, self.context.log)
  36. @classmethod
  37. def global_subsystems(cls):
  38. return super(PythonTask, cls).global_subsystems() + cls._subsystems
  39. @classmethod
  40. def register_options(cls, register):
  41. super(PythonCheckStyleTask, cls).register_options(register)
  42. register('--severity', fingerprint=True, default='COMMENT', type=str,
  43. help='Only messages at this severity or higher are logged. [COMMENT WARNING ERROR].')
  44. register('--strict', fingerprint=True, type=bool,
  45. help='If enabled, have non-zero exit status for any nit at WARNING or higher.')
  46. # Skip short circuits before fingerprinting
  47. register('--skip', type=bool,
  48. help='If enabled, skip this style checker.')
  49. register('--suppress', fingerprint=True, type=file_option, default=None,
  50. help='Takes a XML file where specific rules on specific files will be skipped.')
  51. register('--fail', fingerprint=True, default=True, type=bool,
  52. help='Prevent test failure but still produce output for problems.')
  53. @classmethod
  54. def supports_passthru_args(cls):
  55. return True
  56. def _is_checked(self, target):
  57. return isinstance(target, PythonTarget) and target.has_sources(self._PYTHON_SOURCE_EXTENSION)
  58. @classmethod
  59. def clear_plugins(cls):
  60. """Clear all current plugins registered."""
  61. cls._plugins = []
  62. @classmethod
  63. def register_plugin(cls, name, subsystem):
  64. """Register plugin to be run as part of Python Style checks.
  65. :param string name: Name of the plugin.
  66. :param PluginSubsystemBase subsystem: Plugin subsystem subclass.
  67. """
  68. plugin = LintPlugin(name=name, subsystem=subsystem)
  69. cls._plugins.append(plugin)
  70. cls._subsystems += (plugin.subsystem, )
  71. def get_nits(self, filename):
  72. """Iterate over the instances style checker and yield Nits.
  73. :param filename: str pointing to a file within the buildroot.
  74. """
  75. try:
  76. python_file = PythonFile.parse(filename, root=get_buildroot())
  77. except CheckSyntaxError as e:
  78. yield e.as_nit()
  79. return
  80. if noqa_file_filter(python_file):
  81. return
  82. if self.options.suppress:
  83. # Filter out any suppressed plugins
  84. check_plugins = [plugin for plugin in self._plugins
  85. if self.excluder.should_include(filename, plugin.name)]
  86. else:
  87. check_plugins = self._plugins
  88. for plugin in check_plugins:
  89. for i, nit in enumerate(plugin.checker(python_file)):
  90. if i == 0:
  91. # NB: Add debug log header for nits from each plugin, but only if there are nits from it.
  92. self.context.log.debug('Nits from plugin {} for {}'.format(plugin.name, filename))
  93. if not nit.has_lines_to_display:
  94. yield nit
  95. continue
  96. if all(not line_contains_noqa(line) for line in nit.lines):
  97. yield nit
  98. def check_file(self, filename):
  99. """Process python file looking for indications of problems.
  100. :param filename: (str) Python source filename
  101. :return: (int) number of failures
  102. """
  103. # If the user specifies an invalid severity use comment.
  104. log_threshold = Nit.SEVERITY.get(self.options.severity, Nit.COMMENT)
  105. failure_count = 0
  106. fail_threshold = Nit.WARNING if self.options.strict else Nit.ERROR
  107. for i, nit in enumerate(self.get_nits(filename)):
  108. if i == 0:
  109. print() # Add an extra newline to clean up the output only if we have nits.
  110. if nit.severity >= log_threshold:
  111. print('{nit}\n'.format(nit=nit))
  112. if nit.severity >= fail_threshold:
  113. failure_count += 1
  114. return failure_count
  115. def checkstyle(self, sources):
  116. """Iterate over sources and run checker on each file.
  117. Files can be suppressed with a --suppress option which takes an xml file containing
  118. file paths that have exceptions and the plugins they need to ignore.
  119. :param sources: iterable containing source file names.
  120. :return: (int) number of failures
  121. """
  122. failure_count = 0
  123. for filename in sources:
  124. failure_count += self.check_file(filename)
  125. if failure_count > 0 and self.options.fail:
  126. raise TaskError(
  127. '{} Python Style issues found. For import order related issues, please try '
  128. '`./pants fmt.isort <targets>`'.format(failure_count))
  129. return failure_count
  130. def execute(self):
  131. """Run Checkstyle on all found source files."""
  132. if self.options.skip:
  133. return
  134. with self.invalidated(self.context.targets(self._is_checked)) as invalidation_check:
  135. sources = self.calculate_sources([vt.target for vt in invalidation_check.invalid_vts])
  136. if sources:
  137. return self.checkstyle(sources)
  138. def calculate_sources(self, targets):
  139. """Generate a set of source files from the given targets."""
  140. sources = set()
  141. for target in targets:
  142. sources.update(
  143. source for source in target.sources_relative_to_buildroot()
  144. if source.endswith(self._PYTHON_SOURCE_EXTENSION)
  145. )
  146. return sources
  147. register_plugins(PythonCheckStyleTask)