"""Checker for noqa comments.""" from __future__ import annotations import enum import re import tokenize from typing import ClassVar, TYPE_CHECKING import flake8_noqa from typing_extensions import Protocol from . import noqa_filter from .noqa_comment import FileComment, InlineComment if (TYPE_CHECKING): from collections.abc import Iterator, Sequence try: try: from importlib.metadata import version except ModuleNotFoundError: # python < 3.8 use polyfill from importlib_metadata import version # type: ignore package_version = version(__package__) except Exception: package_version = 'unknown' SINGLE_SPACE = re.compile(r' [^\s]') class Message(enum.Enum): """Messages.""" INLINE_NOQA_BAD_SPACE = (1, '"#{noqa}{sep}{codes}" must have a single space after the hash, e.g. "# {noqa_strip}{sep_codes_strip}"') INLINE_NOQA_NO_COLON = (2, '"#{noqa}{codes}" must have a colon, e.g. "# {noqa_strip}: {codes_strip}"') INLINE_NOQA_BAD_COLON_SPACE = (3, '"#{noqa}{sep}{codes}" must not have a space before the colon, e.g. "# {noqa_strip}: {codes_strip}"') INLINE_NOQA_BAD_CODE_SPACE = (4, '"#{noqa}{sep}{codes}" must have at most one space before the codes, e.g. "# {noqa_strip}: {codes_strip}"') INLINE_NOQA_DUPLICATE_CODE = (5, '"#{noqa}{sep}{codes}" has duplicate codes, remove {duplicates}') FILE_NOQA_BAD_SPACE = (11, '"#{flake8}{sep}{noqa}" must have a single space after the hash, e.g. "# {flake8_strip}{sep_colon}{noqa}"') FILE_NOQA_NO_COLON = (12, '"#{flake8}{noqa}" must have a colon or equals, e.g. "# {flake8_strip}:{noqa}"') FILE_NOQA_BAD_COLON_SPACE = (13, '"#{flake8}{sep}{noqa}" must not have a space before the {sep_name}, e.g. "# {flake8_strip}{sep_strip}{noqa}"') @property def code(self) -> str: """Get code for message.""" return (flake8_noqa.noqa_checker_prefix + str(self.value[0]).rjust(6 - len(flake8_noqa.noqa_checker_prefix), '0')) def text(self, **kwargs) -> str: """Get formatted text of message.""" return self.value[1].format(**kwargs) class Options(Protocol): """Protocol for options.""" noqa_include_name: bool class NoqaChecker: """Check noqa comments for proper formatting.""" name: ClassVar[str] = __package__.replace('_', '-') version: ClassVar[str] = package_version plugin_name: ClassVar[str] tokens: Sequence[tokenize.TokenInfo] filename: str @classmethod def parse_options(cls, options: Options) -> None: """Parse plugin options.""" cls.plugin_name = (' (' + cls.name + ')') if (options.noqa_include_name) else '' def __init__(self, logical_line: str, tokens: Sequence[tokenize.TokenInfo], filename: str) -> None: self.tokens = tokens self.filename = filename def _message(self, token: tokenize.TokenInfo, message: Message, **kwargs) -> tuple[tuple[int, int], str]: return (token.start, f'{message.code}{self.plugin_name} {message.text(**kwargs)}') def __iter__(self) -> Iterator[tuple[tuple[int, int], str]]: """Primary call from flake8, yield error messages.""" for token in self.tokens: if (tokenize.COMMENT != token.type): continue file_comment = FileComment.match(token) if (file_comment and (not file_comment.valid)): if (not SINGLE_SPACE.match(file_comment.flake8)): yield self._message(token, Message.FILE_NOQA_BAD_SPACE, flake8=file_comment.flake8, flake8_strip=file_comment.flake8.strip(), sep=file_comment.sep, sep_colon=(file_comment.sep or ':'), noqa=file_comment.noqa) if (not file_comment.sep): yield self._message(token, Message.FILE_NOQA_NO_COLON, flake8=file_comment.flake8, flake8_strip=file_comment.flake8.strip(), noqa=file_comment.noqa) else: if (not (file_comment.sep.startswith(':') or file_comment.sep.startswith('='))): yield self._message(token, Message.FILE_NOQA_BAD_COLON_SPACE, flake8=file_comment.flake8, flake8_strip=file_comment.flake8.strip(), sep=file_comment.sep, sep_strip=file_comment.sep.strip(), sep_name='colon' if (':' in file_comment.sep) else 'equals', noqa=file_comment.noqa) inline_comment = InlineComment.match(token, self.tokens[0]) if (inline_comment): noqa_filter.InlineComment.add_comment(self.filename, inline_comment) if (not inline_comment.valid): yield self._message(token, Message.INLINE_NOQA_BAD_SPACE, noqa=inline_comment.noqa, noqa_strip=inline_comment.noqa.strip(), sep=inline_comment.sep, codes=inline_comment.codes, sep_codes_strip=(f': {inline_comment.codes.strip()}' if (inline_comment.codes) else '')) if (inline_comment.codes): if (not inline_comment.sep): yield self._message(token, Message.INLINE_NOQA_NO_COLON, noqa=inline_comment.noqa, noqa_strip=inline_comment.noqa.strip(), sep=inline_comment.sep, codes=inline_comment.codes, codes_strip=inline_comment.codes.strip()) else: if (not inline_comment.sep.startswith(':')): yield self._message(token, Message.INLINE_NOQA_BAD_COLON_SPACE, noqa=inline_comment.noqa, noqa_strip=inline_comment.noqa.strip(), sep=inline_comment.sep, codes=inline_comment.codes, codes_strip=inline_comment.codes.strip()) if ((inline_comment.codes != inline_comment.codes.strip()) and not SINGLE_SPACE.match(inline_comment.codes)): yield self._message(token, Message.INLINE_NOQA_BAD_CODE_SPACE, noqa=inline_comment.noqa, noqa_strip=inline_comment.noqa.strip(), sep=inline_comment.sep, codes=inline_comment.codes, codes_strip=inline_comment.codes.strip()) seen_codes = set() duplicates = [] for code in inline_comment.code_list: if (code in seen_codes): duplicates.append(code) else: seen_codes.add(code) if (duplicates): yield self._message(token, Message.INLINE_NOQA_DUPLICATE_CODE, noqa=inline_comment.noqa, sep=inline_comment.sep, codes=inline_comment.codes, duplicates=', '.join(duplicates))