~ubuntu-branches/debian/sid/python-doc8/sid

« back to all changes in this revision

Viewing changes to doc8/checks.py

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2015-10-14 08:18:40 UTC
  • Revision ID: package-import@ubuntu.com-20151014081840-7hajogar155lmmeb
Tags: upstream-0.6.0
ImportĀ upstreamĀ versionĀ 0.6.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2014 Ivan Melnikov <iv at altlinux dot org>
 
2
#
 
3
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
 
4
#
 
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
 
6
# not use this file except in compliance with the License. You may obtain
 
7
# a copy of the License at
 
8
#
 
9
#      http://www.apache.org/licenses/LICENSE-2.0
 
10
#
 
11
# Unless required by applicable law or agreed to in writing, software
 
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
14
# License for the specific language governing permissions and limitations
 
15
# under the License.
 
16
 
 
17
import abc
 
18
import collections
 
19
import re
 
20
 
 
21
from docutils import nodes as docutils_nodes
 
22
import six
 
23
 
 
24
from doc8 import utils
 
25
 
 
26
 
 
27
@six.add_metaclass(abc.ABCMeta)
 
28
class ContentCheck(object):
 
29
    def __init__(self, cfg):
 
30
        self._cfg = cfg
 
31
 
 
32
    @abc.abstractmethod
 
33
    def report_iter(self, parsed_file):
 
34
        pass
 
35
 
 
36
 
 
37
@six.add_metaclass(abc.ABCMeta)
 
38
class LineCheck(object):
 
39
    def __init__(self, cfg):
 
40
        self._cfg = cfg
 
41
 
 
42
    @abc.abstractmethod
 
43
    def report_iter(self, line):
 
44
        pass
 
45
 
 
46
 
 
47
class CheckTrailingWhitespace(LineCheck):
 
48
    _TRAILING_WHITESPACE_REGEX = re.compile('\s$')
 
49
    REPORTS = frozenset(["D002"])
 
50
 
 
51
    def report_iter(self, line):
 
52
        if self._TRAILING_WHITESPACE_REGEX.search(line):
 
53
            yield ('D002', 'Trailing whitespace')
 
54
 
 
55
 
 
56
class CheckIndentationNoTab(LineCheck):
 
57
    _STARTING_WHITESPACE_REGEX = re.compile('^(\s+)')
 
58
    REPORTS = frozenset(["D003"])
 
59
 
 
60
    def report_iter(self, line):
 
61
        match = self._STARTING_WHITESPACE_REGEX.search(line)
 
62
        if match:
 
63
            spaces = match.group(1)
 
64
            if '\t' in spaces:
 
65
                yield ('D003', 'Tabulation used for indentation')
 
66
 
 
67
 
 
68
class CheckCarriageReturn(LineCheck):
 
69
    REPORTS = frozenset(["D004"])
 
70
 
 
71
    def report_iter(self, line):
 
72
        if "\r" in line:
 
73
            yield ('D004', 'Found literal carriage return')
 
74
 
 
75
 
 
76
class CheckNewlineEndOfFile(ContentCheck):
 
77
    REPORTS = frozenset(["D005"])
 
78
 
 
79
    def __init__(self, cfg):
 
80
        super(CheckNewlineEndOfFile, self).__init__(cfg)
 
81
 
 
82
    def report_iter(self, parsed_file):
 
83
        if parsed_file.lines and not parsed_file.lines[-1].endswith(b'\n'):
 
84
            yield (len(parsed_file.lines), 'D005', 'No newline at end of file')
 
85
 
 
86
 
 
87
class CheckValidity(ContentCheck):
 
88
    REPORTS = frozenset(["D000"])
 
89
    EXT_MATCHER = re.compile(r"(.*)[.]rst", re.I)
 
90
 
 
91
    # From docutils docs:
 
92
    #
 
93
    # Report system messages at or higher than <level>: "info" or "1",
 
94
    # "warning"/"2" (default), "error"/"3", "severe"/"4", "none"/"5"
 
95
    #
 
96
    # See: http://docutils.sourceforge.net/docs/user/config.html#report-level
 
97
    WARN_LEVELS = frozenset([2, 3, 4])
 
98
 
 
99
    # Only used when running in sphinx mode.
 
100
    SPHINX_IGNORES_REGEX = [
 
101
        re.compile(r'^Unknown interpreted text'),
 
102
        re.compile(r'^Unknown directive type'),
 
103
        re.compile(r'^Undefined substitution'),
 
104
        re.compile(r'^Substitution definition contains illegal element'),
 
105
    ]
 
106
 
 
107
    def __init__(self, cfg):
 
108
        super(CheckValidity, self).__init__(cfg)
 
109
        self._sphinx_mode = cfg.get('sphinx')
 
110
 
 
111
    def report_iter(self, parsed_file):
 
112
        for error in parsed_file.errors:
 
113
            if error.level not in self.WARN_LEVELS:
 
114
                continue
 
115
            ignore = False
 
116
            if self._sphinx_mode:
 
117
                for m in self.SPHINX_IGNORES_REGEX:
 
118
                    if m.match(error.message):
 
119
                        ignore = True
 
120
                        break
 
121
            if not ignore:
 
122
                yield (error.line, 'D000', error.message)
 
123
 
 
124
 
 
125
class CheckMaxLineLength(ContentCheck):
 
126
    REPORTS = frozenset(["D001"])
 
127
 
 
128
    def __init__(self, cfg):
 
129
        super(CheckMaxLineLength, self).__init__(cfg)
 
130
        self._max_line_length = self._cfg['max_line_length']
 
131
        self._allow_long_titles = self._cfg['allow_long_titles']
 
132
 
 
133
    def _extract_node_lines(self, doc):
 
134
 
 
135
        def extract_lines(node, start_line):
 
136
            lines = [start_line]
 
137
            if isinstance(node, (docutils_nodes.title)):
 
138
                start = start_line - len(node.rawsource.splitlines())
 
139
                if start >= 0:
 
140
                    lines.append(start)
 
141
            if isinstance(node, (docutils_nodes.literal_block)):
 
142
                end = start_line + len(node.rawsource.splitlines()) - 1
 
143
                lines.append(end)
 
144
            return lines
 
145
 
 
146
        def gather_lines(node):
 
147
            lines = []
 
148
            for n in node.traverse(include_self=True):
 
149
                lines.extend(extract_lines(n, find_line(n)))
 
150
            return lines
 
151
 
 
152
        def find_line(node):
 
153
            n = node
 
154
            while n is not None:
 
155
                if n.line is not None:
 
156
                    return n.line
 
157
                n = n.parent
 
158
            return None
 
159
 
 
160
        def filter_systems(node):
 
161
            if utils.has_any_node_type(node, (docutils_nodes.system_message,)):
 
162
                return False
 
163
            return True
 
164
 
 
165
        nodes_lines = []
 
166
        first_line = -1
 
167
        for n in utils.filtered_traverse(doc, filter_systems):
 
168
            line = find_line(n)
 
169
            if line is None:
 
170
                continue
 
171
            if first_line == -1:
 
172
                first_line = line
 
173
            contained_lines = set(gather_lines(n))
 
174
            nodes_lines.append((n, (min(contained_lines),
 
175
                                    max(contained_lines))))
 
176
        return (nodes_lines, first_line)
 
177
 
 
178
    def _extract_directives(self, lines):
 
179
 
 
180
        def starting_whitespace(line):
 
181
            m = re.match(r"^(\s+)(.*)$", line)
 
182
            if not m:
 
183
                return 0
 
184
            return len(m.group(1))
 
185
 
 
186
        def all_whitespace(line):
 
187
            return bool(re.match(r"^(\s*)$", line))
 
188
 
 
189
        def find_directive_end(start, lines):
 
190
            after_lines = collections.deque(lines[start + 1:])
 
191
            k = 0
 
192
            while after_lines:
 
193
                line = after_lines.popleft()
 
194
                if all_whitespace(line) or starting_whitespace(line) >= 1:
 
195
                    k += 1
 
196
                else:
 
197
                    break
 
198
            return start + k
 
199
 
 
200
        # Find where directives start & end so that we can exclude content in
 
201
        # these directive regions (the rst parser may not handle this correctly
 
202
        # for unknown directives, so we have to do it manually).
 
203
        directives = []
 
204
        for i, line in enumerate(lines):
 
205
            if re.match(r"^..\s(.*?)::\s*", line):
 
206
                directives.append((i, find_directive_end(i, lines)))
 
207
            elif re.match(r"^::\s*$", line):
 
208
                directives.append((i, find_directive_end(i, lines)))
 
209
        return directives
 
210
 
 
211
    def _txt_checker(self, parsed_file):
 
212
        for i, line in enumerate(parsed_file.lines_iter()):
 
213
            if len(line) > self._max_line_length:
 
214
                if not utils.contains_url(line):
 
215
                    yield (i + 1, 'D001', 'Line too long')
 
216
 
 
217
    def _rst_checker(self, parsed_file):
 
218
        lines = list(parsed_file.lines_iter())
 
219
        doc = parsed_file.document
 
220
        nodes_lines, first_line = self._extract_node_lines(doc)
 
221
        directives = self._extract_directives(lines)
 
222
 
 
223
        def find_containing_nodes(num):
 
224
            if num < first_line and len(nodes_lines):
 
225
                return [nodes_lines[0][0]]
 
226
            contained_in = []
 
227
            for (n, (line_min, line_max)) in nodes_lines:
 
228
                if num >= line_min and num <= line_max:
 
229
                    contained_in.append((n, (line_min, line_max)))
 
230
            smallest_span = None
 
231
            best_nodes = []
 
232
            for (n, (line_min, line_max)) in contained_in:
 
233
                span = line_max - line_min
 
234
                if smallest_span is None:
 
235
                    smallest_span = span
 
236
                    best_nodes = [n]
 
237
                elif span < smallest_span:
 
238
                    smallest_span = span
 
239
                    best_nodes = [n]
 
240
                elif span == smallest_span:
 
241
                    best_nodes.append(n)
 
242
            return best_nodes
 
243
 
 
244
        def any_types(nodes, types):
 
245
            return any([isinstance(n, types) for n in nodes])
 
246
 
 
247
        skip_types = (
 
248
            docutils_nodes.target,
 
249
            docutils_nodes.literal_block,
 
250
        )
 
251
        title_types = (
 
252
            docutils_nodes.title,
 
253
            docutils_nodes.subtitle,
 
254
            docutils_nodes.section,
 
255
        )
 
256
        for i, line in enumerate(lines):
 
257
            if len(line) > self._max_line_length:
 
258
                in_directive = False
 
259
                for (start, end) in directives:
 
260
                    if i >= start and i <= end:
 
261
                        in_directive = True
 
262
                        break
 
263
                if in_directive:
 
264
                    continue
 
265
                stripped = line.lstrip()
 
266
                if ' ' not in stripped:
 
267
                    # No room to split even if we could.
 
268
                    continue
 
269
                if utils.contains_url(stripped):
 
270
                    continue
 
271
                nodes = find_containing_nodes(i + 1)
 
272
                if any_types(nodes, skip_types):
 
273
                    continue
 
274
                if self._allow_long_titles and any_types(nodes, title_types):
 
275
                    continue
 
276
                yield (i + 1, 'D001', 'Line too long')
 
277
 
 
278
    def report_iter(self, parsed_file):
 
279
        if parsed_file.extension.lower() != '.rst':
 
280
            checker_func = self._txt_checker
 
281
        else:
 
282
            checker_func = self._rst_checker
 
283
        for issue in checker_func(parsed_file):
 
284
            yield issue