| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- """Implementation of the StyleGuide used by Flake8."""
- import argparse
- import contextlib
- import copy
- import enum
- import functools
- import itertools
- import logging
- from typing import Dict
- from typing import Generator
- from typing import List
- from typing import Optional
- from typing import Sequence
- from typing import Set
- from typing import Tuple
- from typing import Union
- from flake8 import defaults
- from flake8 import statistics
- from flake8 import utils
- from flake8.formatting import base as base_formatter
- from flake8.violation import Violation
- __all__ = ("StyleGuide",)
- LOG = logging.getLogger(__name__)
- class Selected(enum.Enum):
- """Enum representing an explicitly or implicitly selected code."""
- Explicitly = "explicitly selected"
- Implicitly = "implicitly selected"
- class Ignored(enum.Enum):
- """Enum representing an explicitly or implicitly ignored code."""
- Explicitly = "explicitly ignored"
- Implicitly = "implicitly ignored"
- class Decision(enum.Enum):
- """Enum representing whether a code should be ignored or selected."""
- Ignored = "ignored error"
- Selected = "selected error"
- def _explicitly_chosen(
- *,
- option: Optional[List[str]],
- extend: Optional[List[str]],
- ) -> Tuple[str, ...]:
- ret = [*(option or []), *(extend or [])]
- return tuple(sorted(ret, reverse=True))
- def _select_ignore(
- *,
- option: Optional[List[str]],
- default: Tuple[str, ...],
- extended_default: List[str],
- extend: Optional[List[str]],
- ) -> Tuple[str, ...]:
- # option was explicitly set, ignore the default and extended default
- if option is not None:
- ret = [*option, *(extend or [])]
- else:
- ret = [*default, *extended_default, *(extend or [])]
- return tuple(sorted(ret, reverse=True))
- class DecisionEngine:
- """A class for managing the decision process around violations.
- This contains the logic for whether a violation should be reported or
- ignored.
- """
- def __init__(self, options: argparse.Namespace) -> None:
- """Initialize the engine."""
- self.cache: Dict[str, Decision] = {}
- self.selected_explicitly = _explicitly_chosen(
- option=options.select,
- extend=options.extend_select,
- )
- self.ignored_explicitly = _explicitly_chosen(
- option=options.ignore,
- extend=options.extend_ignore,
- )
- self.selected = _select_ignore(
- option=options.select,
- default=defaults.SELECT,
- extended_default=options.extended_default_select,
- extend=options.extend_select,
- )
- self.ignored = _select_ignore(
- option=options.ignore,
- default=defaults.IGNORE,
- extended_default=options.extended_default_ignore,
- extend=options.extend_ignore,
- )
- def was_selected(self, code: str) -> Union[Selected, Ignored]:
- """Determine if the code has been selected by the user.
- :param code: The code for the check that has been run.
- :returns:
- Selected.Implicitly if the selected list is empty,
- Selected.Explicitly if the selected list is not empty and a match
- was found,
- Ignored.Implicitly if the selected list is not empty but no match
- was found.
- """
- if code.startswith(self.selected_explicitly):
- return Selected.Explicitly
- elif code.startswith(self.selected):
- return Selected.Implicitly
- else:
- return Ignored.Implicitly
- def was_ignored(self, code: str) -> Union[Selected, Ignored]:
- """Determine if the code has been ignored by the user.
- :param code:
- The code for the check that has been run.
- :returns:
- Selected.Implicitly if the ignored list is empty,
- Ignored.Explicitly if the ignored list is not empty and a match was
- found,
- Selected.Implicitly if the ignored list is not empty but no match
- was found.
- """
- if code.startswith(self.ignored_explicitly):
- return Ignored.Explicitly
- elif code.startswith(self.ignored):
- return Ignored.Implicitly
- else:
- return Selected.Implicitly
- def make_decision(self, code: str) -> Decision:
- """Decide if code should be ignored or selected."""
- selected = self.was_selected(code)
- ignored = self.was_ignored(code)
- LOG.debug(
- "The user configured %r to be %r, %r",
- code,
- selected,
- ignored,
- )
- if isinstance(selected, Selected) and isinstance(ignored, Selected):
- return Decision.Selected
- elif isinstance(selected, Ignored) and isinstance(ignored, Ignored):
- return Decision.Ignored
- elif (
- selected is Selected.Explicitly
- and ignored is not Ignored.Explicitly
- ):
- return Decision.Selected
- elif (
- selected is not Selected.Explicitly
- and ignored is Ignored.Explicitly
- ):
- return Decision.Ignored
- elif selected is Ignored.Implicitly and ignored is Selected.Implicitly:
- return Decision.Ignored
- elif (
- selected is Selected.Explicitly and ignored is Ignored.Explicitly
- ) or (
- selected is Selected.Implicitly and ignored is Ignored.Implicitly
- ):
- # we only get here if it was in both lists: longest prefix wins
- select = next(s for s in self.selected if code.startswith(s))
- ignore = next(s for s in self.ignored if code.startswith(s))
- if len(select) > len(ignore):
- return Decision.Selected
- else:
- return Decision.Ignored
- else:
- raise AssertionError(f"unreachable {code} {selected} {ignored}")
- def decision_for(self, code: str) -> Decision:
- """Return the decision for a specific code.
- This method caches the decisions for codes to avoid retracing the same
- logic over and over again. We only care about the select and ignore
- rules as specified by the user in their configuration files and
- command-line flags.
- This method does not look at whether the specific line is being
- ignored in the file itself.
- :param code: The code for the check that has been run.
- """
- decision = self.cache.get(code)
- if decision is None:
- decision = self.make_decision(code)
- self.cache[code] = decision
- LOG.debug('"%s" will be "%s"', code, decision)
- return decision
- class StyleGuideManager:
- """Manage multiple style guides for a single run."""
- def __init__(
- self,
- options: argparse.Namespace,
- formatter: base_formatter.BaseFormatter,
- decider: Optional[DecisionEngine] = None,
- ) -> None:
- """Initialize our StyleGuide.
- .. todo:: Add parameter documentation.
- """
- self.options = options
- self.formatter = formatter
- self.stats = statistics.Statistics()
- self.decider = decider or DecisionEngine(options)
- self.style_guides: List[StyleGuide] = []
- self.default_style_guide = StyleGuide(
- options, formatter, self.stats, decider=decider
- )
- self.style_guides = list(
- itertools.chain(
- [self.default_style_guide],
- self.populate_style_guides_with(options),
- )
- )
- self.style_guide_for = functools.lru_cache(maxsize=None)(
- self._style_guide_for
- )
- def populate_style_guides_with(
- self, options: argparse.Namespace
- ) -> Generator["StyleGuide", None, None]:
- """Generate style guides from the per-file-ignores option.
- :param options:
- The original options parsed from the CLI and config file.
- :returns:
- A copy of the default style guide with overridden values.
- """
- per_file = utils.parse_files_to_codes_mapping(options.per_file_ignores)
- for filename, violations in per_file:
- yield self.default_style_guide.copy(
- filename=filename, extend_ignore_with=violations
- )
- def _style_guide_for(self, filename: str) -> "StyleGuide":
- """Find the StyleGuide for the filename in particular."""
- return max(
- (g for g in self.style_guides if g.applies_to(filename)),
- key=lambda g: len(g.filename or ""),
- )
- @contextlib.contextmanager
- def processing_file(
- self, filename: str
- ) -> Generator["StyleGuide", None, None]:
- """Record the fact that we're processing the file's results."""
- guide = self.style_guide_for(filename)
- with guide.processing_file(filename):
- yield guide
- def handle_error(
- self,
- code: str,
- filename: str,
- line_number: int,
- column_number: int,
- text: str,
- physical_line: Optional[str] = None,
- ) -> int:
- """Handle an error reported by a check.
- :param code:
- The error code found, e.g., E123.
- :param filename:
- The file in which the error was found.
- :param line_number:
- The line number (where counting starts at 1) at which the error
- occurs.
- :param column_number:
- The column number (where counting starts at 1) at which the error
- occurs.
- :param text:
- The text of the error message.
- :param physical_line:
- The actual physical line causing the error.
- :returns:
- 1 if the error was reported. 0 if it was ignored. This is to allow
- for counting of the number of errors found that were not ignored.
- """
- guide = self.style_guide_for(filename)
- return guide.handle_error(
- code, filename, line_number, column_number, text, physical_line
- )
- def add_diff_ranges(self, diffinfo: Dict[str, Set[int]]) -> None:
- """Update the StyleGuides to filter out information not in the diff.
- This provides information to the underlying StyleGuides so that only
- the errors in the line number ranges are reported.
- :param diffinfo:
- Dictionary mapping filenames to sets of line number ranges.
- """
- for guide in self.style_guides:
- guide.add_diff_ranges(diffinfo)
- class StyleGuide:
- """Manage a Flake8 user's style guide."""
- def __init__(
- self,
- options: argparse.Namespace,
- formatter: base_formatter.BaseFormatter,
- stats: statistics.Statistics,
- filename: Optional[str] = None,
- decider: Optional[DecisionEngine] = None,
- ):
- """Initialize our StyleGuide.
- .. todo:: Add parameter documentation.
- """
- self.options = options
- self.formatter = formatter
- self.stats = stats
- self.decider = decider or DecisionEngine(options)
- self.filename = filename
- if self.filename:
- self.filename = utils.normalize_path(self.filename)
- self._parsed_diff: Dict[str, Set[int]] = {}
- def __repr__(self) -> str:
- """Make it easier to debug which StyleGuide we're using."""
- return f"<StyleGuide [{self.filename}]>"
- def copy(
- self,
- filename: Optional[str] = None,
- extend_ignore_with: Optional[Sequence[str]] = None,
- ) -> "StyleGuide":
- """Create a copy of this style guide with different values."""
- filename = filename or self.filename
- options = copy.deepcopy(self.options)
- options.extend_ignore = options.extend_ignore or []
- options.extend_ignore.extend(extend_ignore_with or [])
- return StyleGuide(
- options, self.formatter, self.stats, filename=filename
- )
- @contextlib.contextmanager
- def processing_file(
- self, filename: str
- ) -> Generator["StyleGuide", None, None]:
- """Record the fact that we're processing the file's results."""
- self.formatter.beginning(filename)
- yield self
- self.formatter.finished(filename)
- def applies_to(self, filename: str) -> bool:
- """Check if this StyleGuide applies to the file.
- :param filename:
- The name of the file with violations that we're potentially
- applying this StyleGuide to.
- :returns:
- True if this applies, False otherwise
- """
- if self.filename is None:
- return True
- return utils.matches_filename(
- filename,
- patterns=[self.filename],
- log_message=f'{self!r} does %(whether)smatch "%(path)s"',
- logger=LOG,
- )
- def should_report_error(self, code: str) -> Decision:
- """Determine if the error code should be reported or ignored.
- This method only cares about the select and ignore rules as specified
- by the user in their configuration files and command-line flags.
- This method does not look at whether the specific line is being
- ignored in the file itself.
- :param code:
- The code for the check that has been run.
- """
- return self.decider.decision_for(code)
- def handle_error(
- self,
- code: str,
- filename: str,
- line_number: int,
- column_number: int,
- text: str,
- physical_line: Optional[str] = None,
- ) -> int:
- """Handle an error reported by a check.
- :param code:
- The error code found, e.g., E123.
- :param filename:
- The file in which the error was found.
- :param line_number:
- The line number (where counting starts at 1) at which the error
- occurs.
- :param column_number:
- The column number (where counting starts at 1) at which the error
- occurs.
- :param text:
- The text of the error message.
- :param physical_line:
- The actual physical line causing the error.
- :returns:
- 1 if the error was reported. 0 if it was ignored. This is to allow
- for counting of the number of errors found that were not ignored.
- """
- disable_noqa = self.options.disable_noqa
- # NOTE(sigmavirus24): Apparently we're provided with 0-indexed column
- # numbers so we have to offset that here.
- if not column_number:
- column_number = 0
- error = Violation(
- code,
- filename,
- line_number,
- column_number + 1,
- text,
- physical_line,
- )
- error_is_selected = (
- self.should_report_error(error.code) is Decision.Selected
- )
- is_not_inline_ignored = error.is_inline_ignored(disable_noqa) is False
- is_included_in_diff = error.is_in(self._parsed_diff)
- if error_is_selected and is_not_inline_ignored and is_included_in_diff:
- self.formatter.handle(error)
- self.stats.record(error)
- return 1
- return 0
- def add_diff_ranges(self, diffinfo: Dict[str, Set[int]]) -> None:
- """Update the StyleGuide to filter out information not in the diff.
- This provides information to the StyleGuide so that only the errors
- in the line number ranges are reported.
- :param diffinfo:
- Dictionary mapping filenames to sets of line number ranges.
- """
- self._parsed_diff = diffinfo
|