| # Copyright 2013 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| # This module is not automatically loaded by the `cros` helper. The filename | |
| # would need a "cros_" prefix to make that happen. It lives here so that it | |
| # is alongside the cros_lint.py file. | |
| # | |
| # For msg namespaces, the 9xxx should generally be reserved for our own use. | |
| """Additional lint modules loaded by pylint. | |
| This is loaded by pylint directly via its pylintrc file: | |
| load-plugins=chromite.cli.cros.lint | |
| Then pylint will import the register function and call it. So we can have | |
| as many/few checkers as we want in this one module. | |
| """ | |
| import collections | |
| import itertools | |
| import os | |
| import re | |
| import sys | |
| import tokenize | |
| from typing import Optional, Tuple | |
| import astroid | |
| from pylint import config | |
| import pylint.checkers | |
| import pylint.interfaces | |
| from chromite.utils import memoize | |
| # pylint: disable=too-few-public-methods | |
| class DocStringSectionDetails: | |
| """Object to hold details about a docstring section. | |
| e.g. This holds the Args: or Returns: data. | |
| """ | |
| def __init__(self, name=None, header=None, lines=None, lineno=None): | |
| """Initialize. | |
| Args: | |
| name: The name of this section, e.g. "Args". | |
| header: The raw header of this section, e.g. " Args:". | |
| lines: The raw lines making up the section. | |
| lineno: The first line of the section in the overall docstring. | |
| This counts from one and includes the section header line. | |
| """ | |
| self.name = name | |
| self.header = header | |
| self.lines = [] if lines is None else lines | |
| self.lineno = lineno | |
| def __str__(self): | |
| """A human readable string for this object.""" | |
| return "DocStringSectionDetails(%r, %r)" % (self.name, self.lineno) | |
| def __repr__(self): | |
| """A string to quickly identify this object.""" | |
| return "DocStringSectionDetails(%r, %r, %r, %r)" % ( | |
| self.name, | |
| self.header, | |
| self.lines, | |
| self.lineno, | |
| ) | |
| def __eq__(self, other): | |
| """Test whether two DocStringSectionDetails objects are equivalent""" | |
| return ( | |
| self.name == other.name | |
| and self.header == other.header | |
| and self.lines == other.lines | |
| and self.lineno == other.lineno | |
| ) | |
| def _PylintrcConfig(config_file, section, opts): | |
| """Read specific pylintrc settings. | |
| This is a bit hacky. The pylint framework doesn't allow us to access options | |
| outside a Checker's own namespace (self.name), and multiple linters may not | |
| have the same name/options values (since they get globally registered). So | |
| we have to re-read the registered config file and pull out the value we | |
| want. | |
| The other option would be to force people to duplicate settings in the | |
| config files and that's worse. e.g. | |
| [format] | |
| indent-string = ' ' | |
| [doc_string_checker] | |
| indent-string = ' ' | |
| Args: | |
| config_file: Path to the pylintrc file to read. | |
| section: The section to read. | |
| opts: The specific settings to return. | |
| Returns: | |
| A pylint configuration object. Use option_value('...') to read. | |
| """ | |
| class ConfigReader(config.ConfigurationMixIn): | |
| """Dynamic config file reader.""" | |
| name = section | |
| options = opts | |
| def set_current_module(self, config_file): | |
| """Ignore - read_config_file() invokes this for stats.""" | |
| cfg = ConfigReader(config_file=config_file) | |
| cfg.read_config_file() | |
| cfg.load_config_file() | |
| return cfg | |
| class _EncodingExtractError(Exception): | |
| """When we can't extract real values.""" | |
| class EncodingChecker(pylint.checkers.BaseChecker): | |
| """Various encoding enforcements.""" | |
| __implements__ = pylint.interfaces.IAstroidChecker | |
| # pylint: disable=class-missing-docstring,multiple-statements | |
| class _MessageR9150: | |
| pass | |
| # pylint: enable=class-missing-docstring,multiple-statements | |
| name = "encoding_checker" | |
| priority = -1 | |
| msgs = { | |
| "R9150": ( | |
| "Missing explicit encoding='utf-8'", | |
| ("encoding-missing"), | |
| _MessageR9150, | |
| ), | |
| } | |
| options = () | |
| def _extract_mode_encoding( | |
| self, | |
| node: astroid.Call, | |
| encoding_idx: int, | |
| mode_idx: Optional[int] = None, | |
| ) -> Tuple[str, Optional[str]]: | |
| """Extract the mode & encoding settings in the call.""" | |
| mode = "r" | |
| encoding = None | |
| def _extract_value(arg): | |
| if isinstance(arg, astroid.Name): | |
| raise _EncodingExtractError() | |
| return arg.value | |
| if len(node.args) >= encoding_idx + 1: | |
| encoding = _extract_value(node.args[encoding_idx]) | |
| if mode_idx is not None and len(node.args) >= mode_idx + 1: | |
| mode = _extract_value(node.args[mode_idx]) | |
| if node.keywords: | |
| for kw in node.keywords: | |
| if kw.arg == "mode": | |
| mode = _extract_value(kw.value) | |
| elif kw.arg == "encoding": | |
| encoding = _extract_value(kw.value) | |
| # If the caller explicitly set mode=None, ignore it -- it's an error, | |
| # but other lints (e.g. bad-open-mode) handle it for us. | |
| if mode is None: | |
| raise _EncodingExtractError() | |
| return (mode, encoding) | |
| def _check_call( | |
| self, | |
| node: astroid.Call, | |
| encoding_idx: int, | |
| mode_idx: Optional[int] = None, | |
| ) -> None: | |
| """Check |node| call that has an encoding & mode argument.""" | |
| try: | |
| mode, encoding = self._extract_mode_encoding( | |
| node, encoding_idx, mode_idx | |
| ) | |
| except _EncodingExtractError: | |
| # If we couldn't reliably probe the values, give up. | |
| return | |
| if "b" not in mode and encoding != "utf-8": | |
| self.add_message("R9150", node=node, line=node.fromlineno) | |
| def visit_call(self, node: astroid.Call) -> None: | |
| """Check |node| call.""" | |
| # Check open() calls. | |
| # open(file, mode='r', buffering=-1, encoding=None, ... | |
| if isinstance(node.func, astroid.Name) and node.func.name == "open": | |
| self._check_call(node, 3, 1) | |
| # Check pathlib APIs. | |
| pathlib_calls = { | |
| # Path.open(mode='r', buffering=-1, encoding=None, ... | |
| "open": (2, 0), | |
| # Path.read_text(encoding=None, ... | |
| "read_text": (0, None), | |
| # Path.write_text(data, encoding=None, ... | |
| "write_text": (1, None), | |
| } | |
| if ( | |
| isinstance(node.func, astroid.Attribute) | |
| and node.func.attrname in pathlib_calls | |
| ): | |
| encoding_idx, mode_idx = pathlib_calls[node.func.attrname] | |
| try: | |
| for inode in node.func.infer(): | |
| if ( | |
| isinstance(inode, astroid.bases.BoundMethod) | |
| and inode.bound.pytype() == "pathlib.Path" | |
| ): | |
| self._check_call(node, encoding_idx, mode_idx) | |
| except astroid.exceptions.InferenceError: | |
| # If astroid couldn't guess the type, it will often throw errors | |
| # instead of returning None. We only care about code we can | |
| # guess at with high confidence. | |
| pass | |
| # Check gzip.open() calls. | |
| # NB: The default mode is binary, not text, e.g. "r" == "rb". | |
| # gzip.open(file, mode='r', compresslevel=0, encoding=None, ... | |
| if ( | |
| isinstance(node.func, astroid.Attribute) | |
| and node.func.attrname == "open" | |
| and isinstance(node.func.expr, astroid.Name) | |
| and node.func.expr.name == "gzip" | |
| ): | |
| try: | |
| mode, encoding = self._extract_mode_encoding(node, 3, 1) | |
| except _EncodingExtractError: | |
| return | |
| if "t" in mode and encoding != "utf-8": | |
| self.add_message("R9150", node=node, line=node.fromlineno) | |
| class MonkeypatchChecker(pylint.checkers.BaseChecker): | |
| """Various monkeypatch enforcements.""" | |
| __implements__ = pylint.interfaces.IAstroidChecker | |
| # pylint: disable=class-missing-docstring,multiple-statements | |
| class _MessageR9160: | |
| pass | |
| # pylint: enable=class-missing-docstring,multiple-statements | |
| name = "monkeypatch_checker" | |
| priority = -1 | |
| msgs = { | |
| "R9160": ( | |
| "Do not monkeypatch cros_build_lib.%(func)s; use run_mock instead", | |
| "monkeypatch-cros-build-lib-run", | |
| _MessageR9160, | |
| ), | |
| } | |
| options = () | |
| def visit_call(self, node: astroid.Call) -> None: | |
| """Check |node| call.""" | |
| # Only look for monkeypatch.setattr(...) calls. | |
| # Or self.monkeypatch.setattr(...) calls in unittest.TestCase. | |
| if not ( | |
| isinstance(node.func, astroid.Attribute) | |
| and node.func.attrname == "setattr" | |
| and node.args | |
| and ( | |
| ( | |
| isinstance(node.func.expr, astroid.Attribute) | |
| and node.func.expr.attrname == "monkeypatch" | |
| ) | |
| or ( | |
| isinstance(node.func.expr, astroid.Name) | |
| and node.func.expr.name == "monkeypatch" | |
| ) | |
| ) | |
| ): | |
| return | |
| func = None | |
| arg0 = node.args[0] | |
| if isinstance(arg0, astroid.Const): | |
| # monkeypatch.setattr(<string>) where <string> is module & func. | |
| if arg0.value == "cros_build_lib.run": | |
| func = "run" | |
| elif arg0.value == "cros_build_lib.sudo_run": | |
| func = "sudo_run" | |
| elif isinstance(arg0, astroid.Name): | |
| # monkeypatch.setattr(module, <string>) where <string> is func. | |
| if arg0.name != "cros_build_lib" or len(node.args) < 2: | |
| return | |
| arg1 = node.args[1] | |
| if not ( | |
| isinstance(arg1, astroid.Const) | |
| and arg1.value in ("run", "sudo_run") | |
| ): | |
| return | |
| func = arg1.value | |
| if func is None: | |
| return | |
| self.add_message( | |
| "R9160", node=node, line=node.fromlineno, args={"func": func} | |
| ) | |
| class DocStringChecker(pylint.checkers.BaseChecker): | |
| """PyLint AST based checker to verify PEP 257 compliance | |
| See our style guide for more info: | |
| https://dev.chromium.org/chromium-os/python-style-guidelines#TOC-Describing-arguments-in-docstrings | |
| """ | |
| # TODO: See about merging with the pep257 project: | |
| # https://github.com/GreenSteam/pep257 | |
| __implements__ = pylint.interfaces.IAstroidChecker | |
| # pylint: disable=class-missing-docstring,multiple-statements | |
| class _MessageCP001: | |
| pass | |
| class _MessageCP002: | |
| pass | |
| class _MessageCP003: | |
| pass | |
| class _MessageCP004: | |
| pass | |
| class _MessageCP005: | |
| pass | |
| class _MessageCP006: | |
| pass | |
| class _MessageCP007: | |
| pass | |
| class _MessageCP008: | |
| pass | |
| class _MessageCP009: | |
| pass | |
| class _MessageCP010: | |
| pass | |
| class _MessageCP011: | |
| pass | |
| class _MessageCP012: | |
| pass | |
| class _MessageCP013: | |
| pass | |
| class _MessageCP014: | |
| pass | |
| class _MessageCP015: | |
| pass | |
| class _MessageCP016: | |
| pass | |
| class _MessageCP017: | |
| pass | |
| class _MessageCP018: | |
| pass | |
| class _MessageCP019: | |
| pass | |
| # pylint: enable=class-missing-docstring,multiple-statements | |
| # All the sections we recognize (and in this order). | |
| VALID_FUNC_SECTIONS = ("Examples", "Args", "Returns", "Yields", "Raises") | |
| VALID_CLASS_SECTIONS = ("Examples", "Attributes") | |
| ALL_VALID_SECTIONS = set(VALID_FUNC_SECTIONS + VALID_CLASS_SECTIONS) | |
| # This is the section name in the pylintrc file. | |
| name = "doc_string_checker" | |
| # Any pylintrc config options we accept. | |
| options = () | |
| priority = -1 | |
| MSG_ARGS = "offset:%(offset)i: {%(line)s}" | |
| msgs = { | |
| "C9001": ( | |
| "Modules should have docstrings (even a one liner)", | |
| ("module-missing-docstring"), | |
| _MessageCP001, | |
| ), | |
| "C9002": ( | |
| "Classes should have docstrings (even a one liner)", | |
| ("class-missing-docstring"), | |
| _MessageCP002, | |
| ), | |
| "C9003": ( | |
| "Trailing whitespace in docstring: " + MSG_ARGS, | |
| ("docstring-trailing-whitespace"), | |
| _MessageCP003, | |
| ), | |
| "C9004": ( | |
| "Leading whitespace in docstring (excess or missing)" | |
| ": " + MSG_ARGS, | |
| ("docstring-leading-whitespace"), | |
| _MessageCP004, | |
| ), | |
| "C9005": ( | |
| "Closing triple quotes should not be cuddled", | |
| ("docstring-cuddled-quotes"), | |
| _MessageCP005, | |
| ), | |
| "C9006": ( | |
| "Section names should be preceded by one blank line" | |
| ": " + MSG_ARGS, | |
| ("docstring-section-newline"), | |
| _MessageCP006, | |
| ), | |
| "C9007": ( | |
| 'Section names should be "%(section)s": ' + MSG_ARGS, | |
| ("docstring-section-name"), | |
| _MessageCP007, | |
| ), | |
| "C9008": ( | |
| "Sections should be in the order: %(sections)s", | |
| ("docstring-section-order"), | |
| _MessageCP008, | |
| ), | |
| "C9009": ( | |
| "First line should be a short summary", | |
| ("docstring-first-line"), | |
| _MessageCP009, | |
| ), | |
| "C9010": ( | |
| "Not all args mentioned in doc string: |%(arg)s|", | |
| ("docstring-missing-args"), | |
| _MessageCP010, | |
| ), | |
| "C9011": ( | |
| "Variable args/keywords are named *args/**kwargs, not %(arg)s", | |
| ("docstring-misnamed-args"), | |
| _MessageCP011, | |
| ), | |
| "C9012": ( | |
| "Incorrectly formatted Args section: %(arg)s", | |
| ("docstring-arg-spacing"), | |
| _MessageCP012, | |
| ), | |
| "C9013": ( | |
| "Too many blank lines in a row: " + MSG_ARGS, | |
| ("docstring-too-many-newlines"), | |
| _MessageCP013, | |
| ), | |
| "C9014": ( | |
| "Second line should be blank", | |
| ("docstring-second-line-blank"), | |
| _MessageCP014, | |
| ), | |
| "C9015": ( | |
| "Section indentation should be %(want_indent)s spaces, not " | |
| "%(curr_indent)s spaces: " + MSG_ARGS, | |
| ("docstring-section-indent"), | |
| _MessageCP015, | |
| ), | |
| "C9016": ( | |
| "Closing triple quotes should be indented with %(want_indent)s " | |
| "spaces, not %(curr_indent)s", | |
| ("docstring-trailing-quotes"), | |
| _MessageCP016, | |
| ), | |
| "C9017": ( | |
| "Section %(section)s shows up more than once; previous at " | |
| "%(line_old)i", | |
| ("docstring-duplicate-section"), | |
| _MessageCP017, | |
| ), | |
| "C9018": ( | |
| "Docstrings must start with exactly three quotes", | |
| ("docstring-extra-quotes"), | |
| _MessageCP018, | |
| ), | |
| "C9019": ( | |
| "Use typing module instead of docstring annotations", | |
| ("docstring-deprecated-annotations"), | |
| _MessageCP019, | |
| ), | |
| "C9020": ( | |
| "Duplicate arg documentation found: |%(arg)s|", | |
| ("docstring-duplicate-argument"), | |
| _MessageCP019, | |
| ), | |
| "C9021": ( | |
| "Unknown arg documentation found: |%(arg)s|", | |
| ("docstring-unknown-argument"), | |
| _MessageCP019, | |
| ), | |
| } | |
| def __init__(self, *args, **kwargs): | |
| pylint.checkers.BaseChecker.__init__(self, *args, **kwargs) | |
| if self.linter is None: | |
| # Unit tests don't set this up. | |
| self._indent_string = " " | |
| else: | |
| cfg = _PylintrcConfig( | |
| self.linter.config_file, | |
| "format", | |
| (("indent-string", {"default": " ", "type": "string"}),), | |
| ) | |
| self._indent_string = cfg.option_value("indent-string") | |
| self._indent_len = len(self._indent_string) | |
| def visit_functiondef(self, node): | |
| """Verify function docstrings""" | |
| if node.doc: | |
| lines = node.doc.split("\n") | |
| self._check_common(node, lines) | |
| sections = self._parse_docstring_sections(node, lines) | |
| self._check_section_lines( | |
| node, lines, sections, self.VALID_FUNC_SECTIONS | |
| ) | |
| self._check_all_args_in_doc(node, lines, sections) | |
| self._check_func_signature(node) | |
| else: | |
| # This is what C0111 already does for us, so ignore. | |
| pass | |
| def visit_module(self, node): | |
| """Verify module docstrings""" | |
| if node.doc: | |
| self._check_common(node) | |
| else: | |
| # Ignore stub __init__.py files. | |
| if os.path.basename(node.file) == "__init__.py": | |
| return | |
| self.add_message("C9001", node=node) | |
| def visit_classdef(self, node): | |
| """Verify class docstrings""" | |
| if node.doc: | |
| lines = node.doc.split("\n") | |
| self._check_common(node, lines) | |
| sections = self._parse_docstring_sections(node, lines) | |
| self._check_section_lines( | |
| node, lines, sections, self.VALID_CLASS_SECTIONS | |
| ) | |
| else: | |
| self.add_message("C9002", node=node, line=node.fromlineno) | |
| def _docstring_indent(self, node): | |
| """How much a |node|'s docstring should be indented""" | |
| if node.display_type() == "Module": | |
| return 0 | |
| else: | |
| return node.col_offset + self._indent_len | |
| def _check_common(self, node, lines=None): | |
| """Common checks we enforce on all docstrings""" | |
| if lines is None: | |
| lines = node.doc.split("\n") | |
| funcs = ( | |
| self._check_first_line, | |
| self._check_second_line_blank, | |
| self._check_whitespace, | |
| self._check_last_line, | |
| ) | |
| for f in funcs: | |
| f(node, lines) | |
| def _check_first_line(self, node, lines): | |
| """Make sure first line is a short summary by itself""" | |
| if lines[0] == "": | |
| self.add_message("C9009", node=node, line=node.fromlineno) | |
| # We only check the first line for extra quotes because the grammar | |
| # halts when it sees the next set of triple quotes (which means extra | |
| # trailing quotes are not part of the docstring). | |
| if lines[0].startswith('"'): | |
| self.add_message("C9018", node=node, line=node.fromlineno) | |
| def _check_second_line_blank(self, node, lines): | |
| """Make sure the second line is blank""" | |
| if len(lines) > 1 and lines[1] != "": | |
| self.add_message("C9014", node=node, line=node.fromlineno) | |
| def _check_whitespace(self, node, lines): | |
| """Verify whitespace is correct""" | |
| # Make sure first line doesn't have leading whitespace. | |
| if lines[0].lstrip() != lines[0]: | |
| margs = {"offset": 0, "line": lines[0]} | |
| self.add_message( | |
| "C9004", node=node, line=node.fromlineno, args=margs | |
| ) | |
| # Verify no trailing whitespace. | |
| # We skip the last line since it's supposed to be pure whitespace. | |
| # | |
| # Also check for multiple blank lines in a row. | |
| last_blank = False | |
| for i, l in enumerate(lines[:-1]): | |
| margs = {"offset": i, "line": l} | |
| if l.rstrip() != l: | |
| self.add_message( | |
| "C9003", node=node, line=node.fromlineno, args=margs | |
| ) | |
| curr_blank = l == "" | |
| if last_blank and curr_blank: | |
| self.add_message( | |
| "C9013", node=node, line=node.fromlineno, args=margs | |
| ) | |
| last_blank = curr_blank | |
| # Now specially handle the last line. | |
| l = lines[-1] | |
| if l.strip() != "" and l.rstrip() != l: | |
| margs = {"offset": len(lines), "line": l} | |
| self.add_message( | |
| "C9003", node=node, line=node.fromlineno, args=margs | |
| ) | |
| def _check_last_line(self, node, lines): | |
| """Make sure last line is all by itself""" | |
| if len(lines) > 1: | |
| indent = self._docstring_indent(node) | |
| if lines[-1].strip() != "": | |
| self.add_message("C9005", node=node, line=node.fromlineno) | |
| elif lines[-1] != " " * indent: | |
| # The -1 line holds the """ itself and that should be indented. | |
| margs = { | |
| "offset": len(lines) - 1, | |
| "line": lines[-1], | |
| "want_indent": indent, | |
| "curr_indent": len(lines[-1]), | |
| } | |
| self.add_message( | |
| "C9016", node=node, line=node.fromlineno, args=margs | |
| ) | |
| # The last line should not be blank. | |
| if lines[-2] == "": | |
| margs = {"offset": len(lines) - 2, "line": lines[-2]} | |
| self.add_message( | |
| "C9003", node=node, line=node.fromlineno, args=margs | |
| ) | |
| @memoize.MemoizedSingleCall | |
| def _invalid_sections_map(self): | |
| """Get a mapping from common invalid section names to the valid name.""" | |
| invalid_sections_sets = { | |
| # Handle common misnamings. | |
| "Examples": { | |
| "example", | |
| "exaple", | |
| "exaples", | |
| "usage", | |
| "example usage", | |
| }, | |
| "Attributes": { | |
| "attr", | |
| "attrs", | |
| "attribute", | |
| "prop", | |
| "props", | |
| "properties", | |
| "member", | |
| "members", | |
| }, | |
| "Args": { | |
| "arg", | |
| "argument", | |
| "arguments", | |
| "param", | |
| "params", | |
| "parameter", | |
| "parameters", | |
| }, | |
| "Returns": { | |
| "ret", | |
| "rets", | |
| "return", | |
| "retrun", | |
| "retruns", | |
| "result", | |
| "results", | |
| }, | |
| "Yields": {"yield", "yeild", "yeilds"}, | |
| "Raises": {"raise", "riase", "riases", "throw", "throws"}, | |
| } | |
| invalid_sections_map = {} | |
| for key, value in invalid_sections_sets.items(): | |
| invalid_sections_map.update((v, key) for v in value) | |
| return invalid_sections_map | |
| def _parse_docstring_sections(self, node, lines): | |
| """Find all the sections and return them | |
| Args: | |
| node: The python object we're checking. | |
| lines: Parsed docstring lines. | |
| Returns: | |
| An ordered dict of sections and their (start, end) line numbers. | |
| The start line does not include the section header itself. | |
| {'Args': [start_line_number, end_line_number], ...} | |
| """ | |
| sections = collections.OrderedDict() | |
| invalid_sections_map = self._invalid_sections_map() | |
| indent_len = self._docstring_indent(node) | |
| in_args_section = False | |
| last_section = None | |
| for lineno, line in enumerate(lines[1:], start=2): | |
| line_indent_len = len(line) - len(line.lstrip(" ")) | |
| margs = { | |
| "offset": lineno, | |
| "line": line, | |
| } | |
| l = line.strip() | |
| # Catch semi-common javadoc style. | |
| if l.startswith("@param"): | |
| margs["section"] = "Args" | |
| self.add_message( | |
| "C9007", node=node, line=node.fromlineno, args=margs | |
| ) | |
| if l.startswith("@return"): | |
| margs["section"] = "Returns" | |
| self.add_message( | |
| "C9007", node=node, line=node.fromlineno, args=margs | |
| ) | |
| # See if we can detect incorrect behavior. | |
| section = l.split(":", 1)[0] | |
| # Remember whether we're currently in the Args: section so we don't | |
| # treat named arguments as sections (e.g. a function has a "returns" | |
| # arg). Use the indentation level to detect the start of the next | |
| # section. | |
| if in_args_section: | |
| in_args_section = indent_len < line_indent_len | |
| if not in_args_section: | |
| # We only parse known invalid & valid sections here. This | |
| # avoids picking up things that look like sections but aren't | |
| # (e.g. "Note:" lines), and avoids running checks on sections we | |
| # don't yet support. | |
| if section.lower() in invalid_sections_map: | |
| margs["section"] = invalid_sections_map[section.lower()] | |
| self.add_message( | |
| "C9007", node=node, line=node.fromlineno, args=margs | |
| ) | |
| elif section in self.ALL_VALID_SECTIONS: | |
| if section in sections: | |
| # We got the same section more than once? | |
| margs_copy = margs.copy() | |
| margs_copy.update( | |
| { | |
| "line_old": sections[section].lineno, | |
| "section": section, | |
| } | |
| ) | |
| self.add_message( | |
| "C9017", | |
| node=node, | |
| line=node.fromlineno, | |
| args=margs_copy, | |
| ) | |
| else: | |
| # Gather the order of the sections. | |
| sections[ | |
| section | |
| ] = last_section = DocStringSectionDetails( | |
| name=section, header=line, lineno=lineno | |
| ) | |
| # Detect whether we're in the Args section once we've processed | |
| # the Args section itself. | |
| in_args_section = section in ("Args", "Attributes") | |
| if l == "" and last_section: | |
| last_section.lines = lines[last_section.lineno : lineno - 1] | |
| last_section = None | |
| return sections | |
| def _check_section_lines(self, node, lines, sections, valid_sections): | |
| """Verify each section (e.g. Args/Returns/etc...) is correct""" | |
| indent_len = self._docstring_indent(node) | |
| # Make sure the sections are in the right order. | |
| found_sections = [x for x in valid_sections if x in sections] | |
| if found_sections != list(sections): | |
| margs = {"sections": ", ".join(valid_sections)} | |
| self.add_message( | |
| "C9008", node=node, line=node.fromlineno, args=margs | |
| ) | |
| for section in sections.values(): | |
| # We're going to check the section line itself. | |
| lineno = section.lineno | |
| line = section.header | |
| line_indent_len = len(line) - len(line.lstrip(" ")) | |
| margs = { | |
| "offset": lineno, | |
| "line": line, | |
| "want_indent": indent_len, | |
| "curr_indent": line_indent_len, | |
| } | |
| # Make sure it has some number of leading whitespace. | |
| if not line.startswith(" "): | |
| self.add_message( | |
| "C9004", node=node, line=node.fromlineno, args=margs | |
| ) | |
| # Make sure it has a single trailing colon. | |
| if line.strip() != "%s:" % section.name: | |
| margs["section"] = section.name | |
| self.add_message( | |
| "C9007", node=node, line=node.fromlineno, args=margs | |
| ) | |
| # Verify blank line before it. We use -2 because lineno counts from | |
| # one, but lines is a zero-based list. | |
| if lines[lineno - 2] != "": | |
| self.add_message( | |
| "C9006", node=node, line=node.fromlineno, args=margs | |
| ) | |
| # Check the indentation level on the section header (e.g. Args:). | |
| if line_indent_len != indent_len: | |
| self.add_message( | |
| "C9015", node=node, line=node.fromlineno, args=margs | |
| ) | |
| # Now check the indentation of subtext in each section. | |
| saw_exact = False | |
| want_indent = indent_len + self._indent_len | |
| margs["want_indent"] = want_indent | |
| first_item_margs = None | |
| for i, line in enumerate(section.lines, start=1): | |
| # Every line should be indented at least the minimum. | |
| # Always update margs so that if we drop through below, it has | |
| # reasonable values when generating the message. | |
| line_indent_len = len(line) - len(line.lstrip(" ")) | |
| if first_item_margs is None: | |
| first_item_margs = { | |
| **margs, | |
| "line": line, | |
| "offset": lineno + i, | |
| "curr_indent": line_indent_len, | |
| } | |
| if line_indent_len == want_indent: | |
| saw_exact = True | |
| elif line_indent_len < want_indent: | |
| self.add_message( | |
| "C9015", | |
| node=node, | |
| line=node.fromlineno, | |
| args={ | |
| **margs, | |
| "line": line, | |
| "offset": lineno + i, | |
| "curr_indent": line_indent_len, | |
| }, | |
| ) | |
| # If none of the lines were indented at the exact level, then | |
| # something is amiss like they're all incorrectly offset. | |
| if first_item_margs and not saw_exact: | |
| self.add_message( | |
| "C9015", | |
| node=node, | |
| line=node.fromlineno, | |
| args=first_item_margs, | |
| ) | |
| def _check_all_args_in_doc(self, node, _lines, sections): | |
| """All function arguments are mentioned in doc""" | |
| if not hasattr(node, "args"): | |
| return | |
| # If they don't have an Args section, then give it a pass. | |
| section = sections.get("Args") | |
| if section is None: | |
| return | |
| # Now verify all args exist. | |
| # TODO: Should we verify arg order matches doc order ? | |
| # TODO: Should we check indentation of wrapped docs ? | |
| missing_args = [] | |
| found_args = set() | |
| for arg in itertools.chain(node.args.args, node.args.kwonlyargs): | |
| # Ignore class related args. | |
| if arg.name in ("cls", "self"): | |
| continue | |
| # Ignore ignored args. | |
| if arg.name.startswith("_"): | |
| continue | |
| # Valid arguments may look like `<arg>:` or `<arg> (<type>):` or | |
| # `<arg>: (<type>)`. | |
| arg_re = re.compile( | |
| r"%s( \([^)]+\))?:( \([^)]+\))?" % re.escape(arg.name) | |
| ) | |
| for l in section.lines: | |
| aline = l.lstrip() | |
| m = arg_re.match(aline) | |
| if m: | |
| if arg.name in found_args: | |
| margs = {"arg": l} | |
| self.add_message( | |
| "C9020", node=node, line=node.fromlineno, args=margs | |
| ) | |
| found_args.add(arg.name) | |
| if m.group(1) is not None or m.group(2) is not None: | |
| margs = {"arg": l} | |
| self.add_message( | |
| "C9019", node=node, line=node.fromlineno, args=margs | |
| ) | |
| amsg = aline[m.end() :] | |
| if amsg and len(amsg) - len(amsg.lstrip()) != 1: | |
| margs = {"arg": l} | |
| self.add_message( | |
| "C9012", node=node, line=node.fromlineno, args=margs | |
| ) | |
| if arg.name not in found_args: | |
| missing_args.append(arg.name) | |
| if missing_args: | |
| margs = {"arg": "|, |".join(missing_args)} | |
| self.add_message( | |
| "C9010", node=node, line=node.fromlineno, args=margs | |
| ) | |
| # Check special named arguments. | |
| args_exp = node.args.vararg == "args" | |
| args_re = re.compile(r"(\*?args):") | |
| args_found = None | |
| kwargs_exp = node.args.kwarg == "kwargs" | |
| kwargs_re = re.compile(r"((\*\*)?kwargs):") | |
| kwargs_found = None | |
| for l in section.lines: | |
| aline = l.lstrip() | |
| m = args_re.match(aline) | |
| if m: | |
| if args_found: | |
| margs = {"arg": l} | |
| self.add_message( | |
| "C9020", node=node, line=node.fromlineno, args=margs | |
| ) | |
| args_found = m.group(1) | |
| m = kwargs_re.match(aline) | |
| if m: | |
| if kwargs_found: | |
| margs = {"arg": l} | |
| self.add_message( | |
| "C9020", node=node, line=node.fromlineno, args=margs | |
| ) | |
| kwargs_found = m.group(1) | |
| # args/kwargs shouldn't be documented if the func doesn't use them. | |
| if args_found == "*args" and not args_exp: | |
| margs = {"arg": args_found} | |
| self.add_message( | |
| "C9021", node=node, line=node.fromlineno, args=margs | |
| ) | |
| if kwargs_found == "**kwargs" and not kwargs_exp: | |
| margs = {"arg": kwargs_found} | |
| self.add_message( | |
| "C9021", node=node, line=node.fromlineno, args=margs | |
| ) | |
| # Only accept "*args" & "**kwargs". | |
| if args_exp and args_found and args_found != "*args": | |
| margs = {"arg": args_found} | |
| self.add_message( | |
| "C9011", node=node, line=node.fromlineno, args=margs | |
| ) | |
| if kwargs_exp and kwargs_found and kwargs_found != "**kwargs": | |
| margs = {"arg": kwargs_found} | |
| self.add_message( | |
| "C9011", node=node, line=node.fromlineno, args=margs | |
| ) | |
| def _check_func_signature(self, node): | |
| """Require *args to be named args, and **kwargs kwargs""" | |
| vararg = node.args.vararg | |
| if vararg and vararg != "args" and vararg != "_args": | |
| margs = {"arg": vararg} | |
| self.add_message( | |
| "C9011", node=node, line=node.fromlineno, args=margs | |
| ) | |
| kwarg = node.args.kwarg | |
| if kwarg and kwarg != "kwargs" and kwarg != "_kwargs": | |
| margs = {"arg": kwarg} | |
| self.add_message( | |
| "C9011", node=node, line=node.fromlineno, args=margs | |
| ) | |
| class SourceChecker(pylint.checkers.BaseChecker): | |
| """Make sure we enforce rules on the source.""" | |
| __implements__ = pylint.interfaces.IAstroidChecker | |
| # pylint: disable=class-missing-docstring,multiple-statements | |
| class _MessageR9200: | |
| pass | |
| class _MessageR9201: | |
| pass | |
| class _MessageR9202: | |
| pass | |
| class _MessageR9203: | |
| pass | |
| class _MessageR9205: | |
| pass | |
| class _MessageR9206: | |
| pass | |
| # pylint: enable=class-missing-docstring,multiple-statements | |
| name = "source_checker" | |
| priority = -1 | |
| MSG_ARGS = "offset:%(offset)i: {%(line)s}" | |
| msgs = { | |
| "R9200": ( | |
| 'Shebang should be "#!/usr/bin/env python3" or ' | |
| '"#!/usr/bin/env python"', | |
| ("bad-shebang"), | |
| _MessageR9200, | |
| ), | |
| "R9201": ( | |
| "Shebang is missing, but file is executable (chmod -x to fix)", | |
| ("missing-shebang"), | |
| _MessageR9201, | |
| ), | |
| "R9202": ( | |
| "Shebang is set, but file is not executable (chmod +x to fix)", | |
| ("spurious-shebang"), | |
| _MessageR9202, | |
| ), | |
| "R9203": ( | |
| "Unittest not named xxx_unittest.py", | |
| ("unittest-misnamed"), | |
| _MessageR9203, | |
| ), | |
| "R9205": ( | |
| "Omit the coding cookie (PEP 263) (Python 3 defaults to utf-8).", | |
| ("omit-coding-cookie"), | |
| _MessageR9205, | |
| ), | |
| "R9206": ( | |
| "Use parens for long line wrapping, not backslashes", | |
| ("parens-not-backslashes"), | |
| _MessageR9206, | |
| ), | |
| } | |
| options = () | |
| # Taken from PEP-263. | |
| _ENCODING_RE = re.compile( | |
| rb"^[ \t\v]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)" | |
| ) | |
| def visit_module(self, node): | |
| """Called when the whole file has been read""" | |
| with node.stream() as stream: | |
| st = os.fstat(stream.fileno()) | |
| self._check_shebang(node, stream, st) | |
| self._check_encoding(stream) | |
| self._check_module_name(node) | |
| self._check_backslashes(node, stream) | |
| def _check_shebang(self, _node, stream, st): | |
| """Verify the shebang is version specific""" | |
| stream.seek(0) | |
| mode = st.st_mode | |
| executable = bool(mode & 0o0111) | |
| shebang = stream.readline() | |
| if shebang[0:2] != b"#!": | |
| if executable: | |
| self.add_message("R9201") | |
| return | |
| elif not executable: | |
| self.add_message("R9202") | |
| if shebang.strip() not in ( | |
| b"#!/usr/bin/env python2", | |
| b"#!/usr/bin/env python3", | |
| b"#!/usr/bin/env python", | |
| b"#!/usr/bin/env vpython", | |
| b"#!/usr/bin/env vpython3", | |
| ): | |
| self.add_message("R9200") | |
| def _check_encoding(self, stream): | |
| """Verify the file has no -*- coding: utf-8 -*- cookie. | |
| In CrOS, all files should be utf-8, which is the Python 3 default. | |
| So these comments are just noise. | |
| See PEP-263 for more details. | |
| https://www.python.org/dev/peps/pep-0263/ | |
| """ | |
| stream.seek(0) | |
| encoding = stream.readline() | |
| # If the first line is the shebang, then the encoding is the second | |
| # line. | |
| if encoding[0:2] == b"#!": | |
| encoding = stream.readline() | |
| # See if the encoding matches the standard. | |
| if self._ENCODING_RE.match(encoding): | |
| self.add_message("R9205") | |
| def _check_module_name(self, node): | |
| """Make sure the module name is correct""" | |
| # Catch various typos. | |
| name = node.name.rsplit(".", 2)[-1] | |
| if name.rsplit("_", 2)[-1] in ("unittests",): | |
| self.add_message("R9203") | |
| def _check_backslashes(self, _node, stream): | |
| """Make sure we use () for line continuations, not backslashes""" | |
| stream.seek(0) | |
| # This is a rough heuristic by nature: try and flag common uses, but not | |
| # all \ usage as there are a few that are legitimate. | |
| matcher = re.compile( | |
| rb"^\s*([^#]*(and|or|=| [-%+|*/])|assert.*) *\\\n$" | |
| ) | |
| for lineno, line in enumerate(stream, start=1): | |
| if matcher.search(line): | |
| s = line.decode("utf8") | |
| self.add_message("R9206", line=lineno, col_offset=len(s) - 1) | |
| class CommentChecker(pylint.checkers.BaseTokenChecker): | |
| """Enforce our arbitrary rules on comments.""" | |
| __implements__ = pylint.interfaces.ITokenChecker | |
| # pylint: disable=class-missing-docstring,multiple-statements | |
| class _MessageR9250: | |
| pass | |
| # pylint: enable=class-missing-docstring,multiple-statements | |
| name = "comment_checker" | |
| priority = -1 | |
| MSG_ARGS = "offset:%(offset)i: {%(line)s}" | |
| msgs = { | |
| "R9250": ( | |
| "One space needed at start of comment: %(comment)s", | |
| ("comment-missing-leading-space"), | |
| _MessageR9250, | |
| ), | |
| } | |
| options = () | |
| def _visit_comment(self, lineno, comment): | |
| """Process |comment| at |lineno|.""" | |
| if comment == "#": | |
| # Ignore standalone comments for spacing. | |
| return | |
| if lineno == 1 and comment.startswith("#!"): | |
| # Ignore shebangs. | |
| return | |
| # We remove multiple leading # to support runs like ###. | |
| if not comment.lstrip("#").startswith(" "): | |
| self.add_message("R9250", line=lineno, args={"comment": comment}) | |
| def process_tokens(self, tokens): | |
| """Process tokens and look for comments.""" | |
| for tok_type, token, (start_row, _), _, _ in tokens: | |
| if tok_type == tokenize.COMMENT: | |
| self._visit_comment(start_row, token) | |
| class FormatStringChecker(pylint.checkers.BaseChecker): | |
| """Check string formatting.""" | |
| __implements__ = pylint.interfaces.IAstroidChecker | |
| # pylint: disable=class-missing-docstring,multiple-statements | |
| class _MessageR9100: | |
| pass | |
| # pylint: enable=class-missing-docstring,multiple-statements | |
| name = "format_string_checker" | |
| priority = -1 | |
| MSG_ARGS = "offset:%(offset)i: {%(line)s}" | |
| msgs = { | |
| "R9100": ( | |
| "Use f-strings or % interpolation, never .format()", | |
| ("banned-string-format-function"), | |
| _MessageR9100, | |
| ), | |
| } | |
| options = () | |
| def visit_const(self, node: astroid.nodes.Const) -> None: | |
| """Process a constant string node.""" | |
| if ( | |
| node.pytype() == "builtins.str" | |
| and not isinstance(node.parent, astroid.nodes.JoinedStr) | |
| and isinstance(node.parent, astroid.nodes.Attribute) | |
| and node.parent.attrname == "format" | |
| and isinstance(node.parent.parent, astroid.nodes.Call) | |
| ): | |
| self.add_message( | |
| "R9100", node=node, line=node.lineno, col_offset=node.col_offset | |
| ) | |
| class ModuleOnlyImportsChecker(pylint.checkers.BaseChecker): | |
| """Checks module members are not imported directly per go/pystyle#imports. | |
| Inspired by github.com/Enforcer/pylint_google_style_guide_imports_enforcing. | |
| """ | |
| __implements__ = pylint.interfaces.IAstroidChecker | |
| # pylint: disable=class-missing-docstring,multiple-statements | |
| class _MessageR9170: | |
| pass | |
| # pylint: enable=class-missing-docstring,multiple-statements | |
| priority = -1 | |
| msgs = { | |
| "R9170": ( | |
| '"%(name)s" shouldn\'t be imported from %(module)s directly.', | |
| ("import-modules-only"), | |
| _MessageR9170, | |
| ), | |
| } | |
| options = () | |
| EXCLUSIONS = frozenset( | |
| { | |
| "typing", | |
| "typing_extensions", | |
| "pathlib", | |
| "collections.abc", | |
| "__future__", | |
| } | |
| ) | |
| def visit_importfrom(self, node: astroid.nodes.ImportFrom): | |
| """Visit an import node.""" | |
| try: | |
| imported = node.do_import_module() | |
| except astroid.AstroidImportError: | |
| # Files that have import statements in `try` blocks will trip up | |
| # pylint here if those statements throw. Skip those imports. | |
| return | |
| if imported.name in self.EXCLUSIONS: | |
| return | |
| for name, _ in node.names: | |
| _, result = imported.lookup(name) | |
| if not result: | |
| # Probably this is another module - fine. | |
| continue | |
| if isinstance(result[0], astroid.nodes.Module): | |
| continue | |
| # Allow submodules. | |
| try: | |
| imported.import_module(name, relative_only=True) | |
| except astroid.AstroidImportError: | |
| self.add_message( | |
| "R9170", | |
| node=node, | |
| line=node.lineno, | |
| col_offset=node.col_offset, | |
| args={"name": name, "module": imported.name}, | |
| ) | |
| def register(linter): | |
| """pylint will call this func to register all our checkers""" | |
| # Walk all the classes in this module and register ours. | |
| this_module = sys.modules[__name__] | |
| for member in dir(this_module): | |
| if not member.endswith("Checker"): | |
| continue | |
| cls = getattr(this_module, member) | |
| linter.register_checker(cls(linter)) |