summaryrefslogtreecommitdiff
path: root/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py
diff options
context:
space:
mode:
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.py185
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))