summaryrefslogtreecommitdiff
path: root/doc/development_guide/how_tos/transform_plugins.rst
blob: e1f7e9e64b411ef9faa9657e68f5c7bf7dde2f0b (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

Transform plugins
^^^^^^^^^^^^^^^^^

Why write a plugin?
-------------------

Pylint is a static analysis tool and Python is a dynamically typed language.
So there will be cases where Pylint cannot analyze files properly (this problem
can happen in statically typed languages also if reflection or dynamic
evaluation is used).

The plugins are a way to tell Pylint how to handle such cases,
since only the user would know what needs to be done. They are usually operating
on the AST level, by modifying or changing it in a way which can ease its
understanding by Pylint.

Example
-------

Let us run Pylint on a module from the Python source: `warnings.py`_ and see what happens:

.. sourcecode:: shell

  amitdev$ pylint -E Lib/warnings.py
  E:297,36: Instance of 'WarningMessage' has no 'message' member (no-member)
  E:298,36: Instance of 'WarningMessage' has no 'filename' member (no-member)
  E:298,51: Instance of 'WarningMessage' has no 'lineno' member (no-member)
  E:298,64: Instance of 'WarningMessage' has no 'line' member (no-member)


Did we catch a genuine error? Let's open the code and look at ``WarningMessage`` class:

.. sourcecode:: python

  class WarningMessage(object):

    """Holds the result of a single showwarning() call."""

    _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file",
                        "line")

    def __init__(self, message, category, filename, lineno, file=None,
                    line=None):
      local_values = locals()
      for attr in self._WARNING_DETAILS:
        setattr(self, attr, local_values[attr])
      self._category_name = category.__name__ if category else None

    def __str__(self):
      ...

Ah, the fields (``message``, ``category`` etc) are not defined statically on the class.
Instead they are added using ``setattr``. Pylint would have a tough time figuring
this out.

Enter Plugin
------------

We can write a transform plugin to tell Pylint how to analyze this properly.

One way to fix our example with a plugin would be to transform the ``WarningMessage`` class,
by setting the attributes so that Pylint can see them. This can be done by
registering a transform function. We can transform any node in the parsed AST like
Module, Class, Function etc. In our case we need to transform a class. It can be done so:

.. sourcecode:: python

  from typing import TYPE_CHECKING

  import astroid

  if TYPE_CHECKING:
      from pylint.lint import PyLinter


  def register(linter: "PyLinter") -> None:
    """This required method auto registers the checker during initialization.

    :param linter: The linter to register the checker to.
    """
    pass

  def transform(cls):
    if cls.name == 'WarningMessage':
      import warnings
      for f in warnings.WarningMessage._WARNING_DETAILS:
        cls.locals[f] = [astroid.ClassDef(f, None)]

  astroid.MANAGER.register_transform(astroid.ClassDef, transform)

Let's go through the plugin. First, we need to register a class transform, which
is done via the ``register_transform`` function in ``MANAGER``. It takes the node
type and function as parameters. We need to change a class, so we use ``astroid.ClassDef``.
We also pass a ``transform`` function which does the actual transformation.

``transform`` function is simple as well. If the class is ``WarningMessage`` then we
add the attributes to its locals (we are not bothered about type of attributes, so setting
them as class will do. But we could set them to any type we want). That's it.

Note: We don't need to do anything in the ``register`` function of the plugin since we
are not modifying anything in the linter itself.

Lets run Pylint with this plugin and see:

.. sourcecode:: bash

  amitdev$ pylint -E --load-plugins warning_plugin Lib/warnings.py
  amitdev$

All the false positives associated with ``WarningMessage`` are now gone. This is just
an example, any code transformation can be done by plugins.

See `astroid/brain`_ for real life examples of transform plugins.

.. _`warnings.py`: https://hg.python.org/cpython/file/2.7/Lib/warnings.py
.. _`astroid/brain`: https://github.com/pylint-dev/astroid/tree/main/astroid/brain