summaryrefslogtreecommitdiffstats
path: root/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py
blob: 1be42f51f236421583d285bcf9164c2520b6ca6f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"""A plugin for pylint to identify imports and functions which should not be used."""
from __future__ import annotations

import os
import typing as t

import astroid

from pylint.checkers import BaseChecker
from pylint.interfaces import IAstroidChecker

ANSIBLE_TEST_MODULES_PATH = os.environ['ANSIBLE_TEST_MODULES_PATH']
ANSIBLE_TEST_MODULE_UTILS_PATH = os.environ['ANSIBLE_TEST_MODULE_UTILS_PATH']


class UnwantedEntry:
    """Defines an unwanted import."""
    def __init__(
            self,
            alternative,  # type: str
            modules_only=False,  # type: bool
            names=None,  # type: t.Optional[t.Tuple[str, ...]]
            ignore_paths=None,  # type: t.Optional[t.Tuple[str, ...]]
            ansible_test_only=False,  # type: bool
    ):  # type: (...) -> None
        self.alternative = alternative
        self.modules_only = modules_only
        self.names = set(names) if names else set()
        self.ignore_paths = ignore_paths
        self.ansible_test_only = ansible_test_only

    def applies_to(self, path, name=None):  # type: (str, t.Optional[str]) -> bool
        """Return True if this entry applies to the given path, otherwise return False."""
        if self.names:
            if not name:
                return False

            if name not in self.names:
                return False

        if self.ignore_paths and any(path.endswith(ignore_path) for ignore_path in self.ignore_paths):
            return False

        if self.ansible_test_only and '/test/lib/ansible_test/_internal/' not in path:
            return False

        if self.modules_only:
            return is_module_path(path)

        return True


def is_module_path(path):  # type: (str) -> bool
    """Return True if the given path is a module or module_utils path, otherwise return False."""
    return path.startswith(ANSIBLE_TEST_MODULES_PATH) or path.startswith(ANSIBLE_TEST_MODULE_UTILS_PATH)


class AnsibleUnwantedChecker(BaseChecker):
    """Checker for unwanted imports and functions."""
    __implements__ = (IAstroidChecker,)

    name = 'unwanted'

    BAD_IMPORT = 'ansible-bad-import'
    BAD_IMPORT_FROM = 'ansible-bad-import-from'
    BAD_FUNCTION = 'ansible-bad-function'
    BAD_MODULE_IMPORT = 'ansible-bad-module-import'

    msgs = dict(
        E5101=('Import %s instead of %s',
               BAD_IMPORT,
               'Identifies imports which should not be used.'),
        E5102=('Import %s from %s instead of %s',
               BAD_IMPORT_FROM,
               'Identifies imports which should not be used.'),
        E5103=('Call %s instead of %s',
               BAD_FUNCTION,
               'Identifies functions which should not be used.'),
        E5104=('Import external package or ansible.module_utils not %s',
               BAD_MODULE_IMPORT,
               'Identifies imports which should not be used.'),
    )

    unwanted_imports = dict(
        # Additional imports that we may want to start checking:
        # boto=UnwantedEntry('boto3', modules_only=True),
        # requests=UnwantedEntry('ansible.module_utils.urls', modules_only=True),
        # urllib=UnwantedEntry('ansible.module_utils.urls', modules_only=True),

        # see https://docs.python.org/2/library/urllib2.html
        urllib2=UnwantedEntry('ansible.module_utils.urls',
                              ignore_paths=(
                                  '/lib/ansible/module_utils/urls.py',
                              )),

        # see https://docs.python.org/3/library/collections.abc.html
        collections=UnwantedEntry('ansible.module_utils.common._collections_compat',
                                  ignore_paths=(
                                      '/lib/ansible/module_utils/common/_collections_compat.py',
                                  ),
                                  names=(
                                      'MappingView',
                                      'ItemsView',
                                      'KeysView',
                                      'ValuesView',
                                      'Mapping', 'MutableMapping',
                                      'Sequence', 'MutableSequence',
                                      'Set', 'MutableSet',
                                      'Container',
                                      'Hashable',
                                      'Sized',
                                      'Callable',
                                      'Iterable',
                                      'Iterator',
                                  )),
    )

    unwanted_functions = {
        # see https://docs.python.org/3/library/tempfile.html#tempfile.mktemp
        'tempfile.mktemp': UnwantedEntry('tempfile.mkstemp'),

        # os.chmod resolves as posix.chmod
        'posix.chmod': UnwantedEntry('verified_chmod',
                                     ansible_test_only=True),

        'sys.exit': UnwantedEntry('exit_json or fail_json',
                                  ignore_paths=(
                                      '/lib/ansible/module_utils/basic.py',
                                      '/lib/ansible/modules/async_wrapper.py',
                                  ),
                                  modules_only=True),

        'builtins.print': UnwantedEntry('module.log or module.debug',
                                        ignore_paths=(
                                            '/lib/ansible/module_utils/basic.py',
                                        ),
                                        modules_only=True),
    }

    def visit_import(self, node):  # type: (astroid.node_classes.Import) -> None
        """Visit an import node."""
        for name in node.names:
            self._check_import(node, name[0])

    def visit_importfrom(self, node):  # type: (astroid.node_classes.ImportFrom) -> None
        """Visit an import from node."""
        self._check_importfrom(node, node.modname, node.names)

    def visit_attribute(self, node):  # type: (astroid.node_classes.Attribute) -> None
        """Visit an attribute node."""
        last_child = node.last_child()

        # this is faster than using type inference and will catch the most common cases
        if not isinstance(last_child, astroid.node_classes.Name):
            return

        module = last_child.name

        entry = self.unwanted_imports.get(module)

        if entry and entry.names:
            if entry.applies_to(self.linter.current_file, node.attrname):
                self.add_message(self.BAD_IMPORT_FROM, args=(node.attrname, entry.alternative, module), node=node)

    def visit_call(self, node):  # type: (astroid.node_classes.Call) -> None
        """Visit a call node."""
        try:
            for i in node.func.inferred():
                func = None

                if isinstance(i, astroid.scoped_nodes.FunctionDef) and isinstance(i.parent, astroid.scoped_nodes.Module):
                    func = '%s.%s' % (i.parent.name, i.name)

                if not func:
                    continue

                entry = self.unwanted_functions.get(func)

                if entry and entry.applies_to(self.linter.current_file):
                    self.add_message(self.BAD_FUNCTION, args=(entry.alternative, func), node=node)
        except astroid.exceptions.InferenceError:
            pass

    def _check_import(self, node, modname):  # type: (astroid.node_classes.Import, str) -> None
        """Check the imports on the specified import node."""
        self._check_module_import(node, modname)

        entry = self.unwanted_imports.get(modname)

        if not entry:
            return

        if entry.applies_to(self.linter.current_file):
            self.add_message(self.BAD_IMPORT, args=(entry.alternative, modname), node=node)

    def _check_importfrom(self, node, modname, names):  # type: (astroid.node_classes.ImportFrom, str, t.List[str]) -> None
        """Check the imports on the specified import from node."""
        self._check_module_import(node, modname)

        entry = self.unwanted_imports.get(modname)

        if not entry:
            return

        for name in names:
            if entry.applies_to(self.linter.current_file, name[0]):
                self.add_message(self.BAD_IMPORT_FROM, args=(name[0], entry.alternative, modname), node=node)

    def _check_module_import(self, node, modname):  # type: (t.Union[astroid.node_classes.Import, astroid.node_classes.ImportFrom], str) -> None
        """Check the module import on the given import or import from node."""
        if not is_module_path(self.linter.current_file):
            return

        if modname == 'ansible.module_utils' or modname.startswith('ansible.module_utils.'):
            return

        if modname == 'ansible' or modname.startswith('ansible.'):
            self.add_message(self.BAD_MODULE_IMPORT, args=(modname,), node=node)


def register(linter):
    """required method to auto register this checker """
    linter.register_checker(AnsibleUnwantedChecker(linter))