diff options
Diffstat (limited to 'test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py')
-rw-r--r-- | test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py | 185 |
1 files changed, 176 insertions, 9 deletions
diff --git a/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py b/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py index 79b8bf15..f6c83373 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +++ b/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py @@ -5,14 +5,31 @@ from __future__ import annotations import datetime +import functools +import json import re +import shlex import typing as t +from tokenize import COMMENT, TokenInfo import astroid -from pylint.interfaces import IAstroidChecker -from pylint.checkers import BaseChecker -from pylint.checkers.utils import check_messages +# support pylint 2.x and 3.x -- remove when supporting only 3.x +try: + from pylint.interfaces import IAstroidChecker, ITokenChecker +except ImportError: + class IAstroidChecker: + """Backwards compatibility for 2.x / 3.x support.""" + + class ITokenChecker: + """Backwards compatibility for 2.x / 3.x support.""" + +try: + from pylint.checkers.utils import check_messages +except ImportError: + from pylint.checkers.utils import only_required_for_messages as check_messages + +from pylint.checkers import BaseChecker, BaseTokenChecker from ansible.module_utils.compat.version import LooseVersion from ansible.module_utils.six import string_types @@ -95,7 +112,7 @@ ANSIBLE_VERSION = LooseVersion('.'.join(ansible_version_raw.split('.')[:3])) def _get_expr_name(node): - """Funciton to get either ``attrname`` or ``name`` from ``node.func.expr`` + """Function to get either ``attrname`` or ``name`` from ``node.func.expr`` Created specifically for the case of ``display.deprecated`` or ``self._display.deprecated`` """ @@ -106,6 +123,17 @@ def _get_expr_name(node): return node.func.expr.name +def _get_func_name(node): + """Function to get either ``attrname`` or ``name`` from ``node.func`` + + Created specifically for the case of ``from ansible.module_utils.common.warnings import deprecate`` + """ + try: + return node.func.attrname + except AttributeError: + return node.func.name + + def parse_isodate(value): """Parse an ISO 8601 date string.""" msg = 'Expected ISO 8601 date string (YYYY-MM-DD)' @@ -118,7 +146,7 @@ def parse_isodate(value): try: return datetime.datetime.strptime(value, '%Y-%m-%d').date() except ValueError: - raise ValueError(msg) + raise ValueError(msg) from None class AnsibleDeprecatedChecker(BaseChecker): @@ -160,6 +188,8 @@ class AnsibleDeprecatedChecker(BaseChecker): self.add_message('ansible-deprecated-date', node=node, args=(date,)) def _check_version(self, node, version, collection_name): + if collection_name is None: + collection_name = 'ansible.builtin' if not isinstance(version, (str, float)): if collection_name == 'ansible.builtin': symbol = 'ansible-invalid-deprecated-version' @@ -197,12 +227,17 @@ class AnsibleDeprecatedChecker(BaseChecker): @property def collection_name(self) -> t.Optional[str]: """Return the collection name, or None if ansible-core is being tested.""" - return self.config.collection_name + return self.linter.config.collection_name @property def collection_version(self) -> t.Optional[SemanticVersion]: """Return the collection version, or None if ansible-core is being tested.""" - return SemanticVersion(self.config.collection_version) if self.config.collection_version is not None else None + if self.linter.config.collection_version is None: + return None + sem_ver = SemanticVersion(self.linter.config.collection_version) + # Ignore pre-release for version comparison to catch issues before the final release is cut. + sem_ver.prerelease = () + return sem_ver @check_messages(*(MSGS.keys())) def visit_call(self, node): @@ -211,8 +246,9 @@ class AnsibleDeprecatedChecker(BaseChecker): date = None collection_name = None try: - if (node.func.attrname == 'deprecated' and 'display' in _get_expr_name(node) or - node.func.attrname == 'deprecate' and _get_expr_name(node)): + funcname = _get_func_name(node) + if (funcname == 'deprecated' and 'display' in _get_expr_name(node) or + funcname == 'deprecate'): if node.keywords: for keyword in node.keywords: if len(node.keywords) == 1 and keyword.arg is None: @@ -258,6 +294,137 @@ class AnsibleDeprecatedChecker(BaseChecker): pass +class AnsibleDeprecatedCommentChecker(BaseTokenChecker): + """Checks for ``# deprecated:`` comments to ensure that the ``version`` + has not passed or met the time for removal + """ + + __implements__ = (ITokenChecker,) + + name = 'deprecated-comment' + msgs = { + 'E9601': ("Deprecated core version (%r) found: %s", + "ansible-deprecated-version-comment", + "Used when a '# deprecated:' comment specifies a version " + "less than or equal to the current version of Ansible", + {'minversion': (2, 6)}), + 'E9602': ("Deprecated comment contains invalid keys %r", + "ansible-deprecated-version-comment-invalid-key", + "Used when a '#deprecated:' comment specifies invalid data", + {'minversion': (2, 6)}), + 'E9603': ("Deprecated comment missing version", + "ansible-deprecated-version-comment-missing-version", + "Used when a '#deprecated:' comment specifies invalid data", + {'minversion': (2, 6)}), + 'E9604': ("Deprecated python version (%r) found: %s", + "ansible-deprecated-python-version-comment", + "Used when a '#deprecated:' comment specifies a python version " + "less than or equal to the minimum python version", + {'minversion': (2, 6)}), + 'E9605': ("Deprecated comment contains invalid version %r: %s", + "ansible-deprecated-version-comment-invalid-version", + "Used when a '#deprecated:' comment specifies an invalid version", + {'minversion': (2, 6)}), + } + + options = ( + ('min-python-version-db', { + 'default': None, + 'type': 'string', + 'metavar': '<path>', + 'help': 'The path to the DB mapping paths to minimum Python versions.', + }), + ) + + def process_tokens(self, tokens: list[TokenInfo]) -> None: + for token in tokens: + if token.type == COMMENT: + self._process_comment(token) + + def _deprecated_string_to_dict(self, token: TokenInfo, string: str) -> dict[str, str]: + valid_keys = {'description', 'core_version', 'python_version'} + data = dict.fromkeys(valid_keys) + for opt in shlex.split(string): + if '=' not in opt: + data[opt] = None + continue + key, _sep, value = opt.partition('=') + data[key] = value + if not any((data['core_version'], data['python_version'])): + self.add_message( + 'ansible-deprecated-version-comment-missing-version', + line=token.start[0], + col_offset=token.start[1], + ) + bad = set(data).difference(valid_keys) + if bad: + self.add_message( + 'ansible-deprecated-version-comment-invalid-key', + line=token.start[0], + col_offset=token.start[1], + args=(','.join(bad),) + ) + return data + + @functools.cached_property + def _min_python_version_db(self) -> dict[str, str]: + """A dictionary of absolute file paths and their minimum required Python version.""" + with open(self.linter.config.min_python_version_db) as db_file: + return json.load(db_file) + + def _process_python_version(self, token: TokenInfo, data: dict[str, str]) -> None: + current_file = self.linter.current_file + check_version = self._min_python_version_db[current_file] + + try: + if LooseVersion(data['python_version']) < LooseVersion(check_version): + self.add_message( + 'ansible-deprecated-python-version-comment', + line=token.start[0], + col_offset=token.start[1], + args=( + data['python_version'], + data['description'] or 'description not provided', + ), + ) + except (ValueError, TypeError) as exc: + self.add_message( + 'ansible-deprecated-version-comment-invalid-version', + line=token.start[0], + col_offset=token.start[1], + args=(data['python_version'], exc) + ) + + def _process_core_version(self, token: TokenInfo, data: dict[str, str]) -> None: + try: + if ANSIBLE_VERSION >= LooseVersion(data['core_version']): + self.add_message( + 'ansible-deprecated-version-comment', + line=token.start[0], + col_offset=token.start[1], + args=( + data['core_version'], + data['description'] or 'description not provided', + ) + ) + except (ValueError, TypeError) as exc: + self.add_message( + 'ansible-deprecated-version-comment-invalid-version', + line=token.start[0], + col_offset=token.start[1], + args=(data['core_version'], exc) + ) + + def _process_comment(self, token: TokenInfo) -> None: + if token.string.startswith('# deprecated:'): + data = self._deprecated_string_to_dict(token, token.string[13:].strip()) + if data['core_version']: + self._process_core_version(token, data) + if data['python_version']: + self._process_python_version(token, data) + + def register(linter): """required method to auto register this checker """ linter.register_checker(AnsibleDeprecatedChecker(linter)) + linter.register_checker(AnsibleDeprecatedCommentChecker(linter)) |