suppression.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. r"""
  2. Each tool has its own method of ignoring errors and warnings.
  3. For example, pylint requires a comment of the form
  4. # pylint disable=<error codes>
  5. PEP8 will not warn on lines with
  6. # noqa
  7. Additionally, flake8 follows that convention for pyflakes errors,
  8. but pyflakes itself does not.
  9. Finally, an entire file is ignored by flake8 if this line is found
  10. in the file:
  11. # flake8\: noqa (the \ is needed to stop prospector ignoring this file :))
  12. This module's job is to attempt to collect all of these methods into
  13. a single coherent list of error suppression locations.
  14. """
  15. import re
  16. import warnings
  17. from collections import defaultdict
  18. from pathlib import Path
  19. from typing import List
  20. from prospector import encoding
  21. from prospector.exceptions import FatalProspectorException
  22. from prospector.message import Message
  23. _FLAKE8_IGNORE_FILE = re.compile(r"flake8[:=]\s*noqa", re.IGNORECASE)
  24. _PEP8_IGNORE_LINE = re.compile(r"#\s+noqa", re.IGNORECASE)
  25. _PYLINT_SUPPRESSED_MESSAGE = re.compile(r"^Suppressed \'([a-z0-9-]+)\' \(from line \d+\)$")
  26. def get_noqa_suppressions(file_contents):
  27. """
  28. Finds all pep8/flake8 suppression messages
  29. :param file_contents:
  30. A list of file lines
  31. :return:
  32. A pair - the first is whether to ignore the whole file, the
  33. second is a set of (0-indexed) line numbers to ignore.
  34. """
  35. ignore_whole_file = False
  36. ignore_lines = set()
  37. for line_number, line in enumerate(file_contents):
  38. if _FLAKE8_IGNORE_FILE.search(line):
  39. ignore_whole_file = True
  40. if _PEP8_IGNORE_LINE.search(line):
  41. ignore_lines.add(line_number + 1)
  42. return ignore_whole_file, ignore_lines
  43. _PYLINT_EQUIVALENTS = {
  44. # TODO: blending has this info already?
  45. "unused-import": (
  46. ("pyflakes", "FL0001"),
  47. ("frosted", "E101"),
  48. )
  49. }
  50. def _parse_pylint_informational(messages: List[Message]):
  51. ignore_files = set()
  52. ignore_messages: dict = defaultdict(lambda: defaultdict(list))
  53. for message in messages:
  54. if message.source == "pylint":
  55. if message.code == "suppressed-message":
  56. # this is a message indicating that a message was raised
  57. # by pylint but suppressed by configuration in the file
  58. match = _PYLINT_SUPPRESSED_MESSAGE.match(message.message)
  59. if not match:
  60. raise FatalProspectorException(f"Could not parsed suppressed message from {message.message}")
  61. suppressed_code = match.group(1)
  62. line_dict = ignore_messages[message.location.path]
  63. line_dict[message.location.line].append(suppressed_code)
  64. elif message.code == "file-ignored":
  65. ignore_files.add(message.location.path)
  66. return ignore_files, ignore_messages
  67. def get_suppressions(filepaths: List[Path], messages):
  68. """
  69. Given every message which was emitted by the tools, and the
  70. list of files to inspect, create a list of files to ignore,
  71. and a map of filepath -> line-number -> codes to ignore
  72. """
  73. paths_to_ignore = set()
  74. lines_to_ignore: dict = defaultdict(set)
  75. messages_to_ignore: dict = defaultdict(lambda: defaultdict(set))
  76. # first deal with 'noqa' style messages
  77. for filepath in filepaths:
  78. try:
  79. file_contents = encoding.read_py_file(filepath).split("\n")
  80. except encoding.CouldNotHandleEncoding as err:
  81. # TODO: this output will break output formats such as JSON
  82. warnings.warn(f"{err.path}: {err.__cause__}", ImportWarning)
  83. continue
  84. ignore_file, ignore_lines = get_noqa_suppressions(file_contents)
  85. if ignore_file:
  86. paths_to_ignore.add(filepath)
  87. lines_to_ignore[filepath] |= ignore_lines
  88. # now figure out which messages were suppressed by pylint
  89. pylint_ignore_files, pylint_ignore_messages = _parse_pylint_informational(messages)
  90. paths_to_ignore |= pylint_ignore_files
  91. for filepath, line in pylint_ignore_messages.items():
  92. for line_number, codes in line.items():
  93. for code in codes:
  94. messages_to_ignore[filepath][line_number].add(("pylint", code))
  95. if code in _PYLINT_EQUIVALENTS:
  96. for equivalent in _PYLINT_EQUIVALENTS[code]:
  97. messages_to_ignore[filepath][line_number].add(equivalent)
  98. return paths_to_ignore, lines_to_ignore, messages_to_ignore