summaryrefslogtreecommitdiff
path: root/src/mailman/testing/flake8.py
blob: 18193d1e1d82df9da63d94e4d709ceb20b63b3d6 (plain) (blame)
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
# Copyright (C) 2016 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
# GNU Mailman is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# GNU Mailman.  If not, see <http://www.gnu.org/licenses/>.

"""Flake8 extensions for Mailman coding style."""


from ast import NodeVisitor
from collections import namedtuple
from enum import Enum


class ImportType(Enum):
    non_from = 0
    from_import = 1


ImportRecord = namedtuple('ImportRecord', 'itype lineno colno, module, names')


class ImportVisitor(NodeVisitor):
    def __init__(self):
        self.imports = []

    def visit_Import(self, node):
        if node.col_offset != 0:
            # Ignore nested imports.
            return
        names = [alias.name for alias in node.names]
        self.imports.append(
            ImportRecord(ImportType.non_from, node.lineno, node.col_offset,
                         None, names))

    def visit_ImportFrom(self, node):
        if node.col_offset != 0:
            # Ignore nested imports.
            return
        names = [alias.name for alias in node.names]
        self.imports.append(
            ImportRecord(ImportType.from_import, node.lineno, node.col_offset,
                         node.module, names))


class ImportOrder:
    name = 'mm-import-order'
    version = '0.1'

    def __init__(self, tree, filename):
        self.tree = tree
        self.filename = filename

    def _error(self, record, code, text):
        return (record.lineno, record.colno,
                '{} {}'.format(code, text), ImportOrder)

    def run(self):
        visitor = ImportVisitor()
        visitor.visit(self.tree)
        last_import = None
        for record in visitor.imports:
            if last_import is None:
                last_import = record
                continue
            if record.itype is ImportType.non_from:
                if len(record.names) != 1:
                    yield self._error(record, 'B402',
                                      'Multiple names on non-from import')
                if last_import.itype is ImportType.from_import:
                    yield self._error(record, 'B401',
                                      'Non-from import follows from-import')
                # Shorter imports should always precede longer import *except*
                # when they are dotted imports and everything but the last
                # path component are the same.  In that case, they should be
                # sorted alphabetically.
                last_name = last_import.names[0]
                this_name = record.names[0]
                if '.' in last_name and '.' in this_name:
                    last_parts = last_name.split('.')
                    this_parts = this_name.split('.')
                    if (last_parts[:-1] == this_parts[:-1] and
                            last_parts[-1] > this_parts[-1]):
                        yield self._error(
                            record, 'B410',
                            'Dotted non-from import not sorted '
                            'alphabetically')
                elif len(last_name) > len(this_name):
                    yield self._error(
                        record, 'B403',
                        'Shorter non-from import follows longer')
                # It's also possible that the imports are the same length, in
                # which case they must be sorted alphabetically.
                if (len(last_import.names[0]) == len(record.names[0]) and
                        last_import.names[0] > record.names[0]):
                    yield self._error(
                        record, 'B404',
                        'Non-from imports not alphabetically sorted')
                if last_import.lineno + 1 != record.lineno:
                    yield self._error(
                        record, 'B405',
                        'Unexpected blank line since last non-from import')
            else:
                assert record.itype is ImportType.from_import
                if (last_import.itype is ImportType.non_from and
                        record.lineno != last_import.lineno + 2):
                    yield self._error(
                        record, 'B406',
                        'Expected one blank line since last non-from import')
                if last_import.itype is ImportType.non_from:
                    last_import = record
                    continue
                if last_import.module > record.module:
                    yield self._error(
                        record, 'B407',
                        'From-imports not sorted alphabetically')
                # All imports from the same module should show up in the same
                # multiline import.
                if last_import.module == record.module:
                    yield self._error(
                        record, 'B408',
                        'Importing from same module on different lines')
                # Check the sort order of the imported names.
                if sorted(record.names) != record.names:
                    yield self._error(
                        record, 'B409',
                        'Imported names are not sorted alphabetically')
                # How to check for no blank lines between from imports?
            # Update the last import.
            last_import = record