Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / pylint / utils.py @ 1227

History | View | Annotate | Download (42.8 KB)

1
# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
2
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
3
#
4
# This program is free software; you can redistribute it and/or modify it under
5
# the terms of the GNU General Public License as published by the Free Software
6
# Foundation; either version 2 of the License, or (at your option) any later
7
# version.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
12
#
13
# You should have received a copy of the GNU General Public License along with
14
# this program; if not, write to the Free Software Foundation, Inc.,
15
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
"""some various utilities and helper classes, most of them used in the
17
main pylint class
18
"""
19
from __future__ import print_function
20

    
21
import collections
22
import itertools
23
import os
24
from os.path import dirname, basename, splitext, exists, isdir, join, normpath
25
import re
26
import sys
27
import tokenize
28
import warnings
29
import textwrap
30

    
31
import six
32
from six.moves import zip  # pylint: disable=redefined-builtin
33

    
34
from astroid import nodes, Module
35
from astroid.modutils import modpath_from_file, get_module_files, \
36
    file_from_modpath, load_module_from_file
37

    
38
from pylint.interfaces import IRawChecker, ITokenChecker, UNDEFINED, implements
39
from pylint.reporters.ureports.nodes import Section
40

    
41

    
42
class UnknownMessage(Exception):
43
    """raised when a unregistered message id is encountered"""
44

    
45
class EmptyReport(Exception):
46
    """raised when a report is empty and so should not be displayed"""
47

    
48

    
49
MSG_TYPES = {
50
    'I' : 'info',
51
    'C' : 'convention',
52
    'R' : 'refactor',
53
    'W' : 'warning',
54
    'E' : 'error',
55
    'F' : 'fatal'
56
    }
57
MSG_TYPES_LONG = {v: k for k, v in six.iteritems(MSG_TYPES)}
58

    
59
MSG_TYPES_STATUS = {
60
    'I' : 0,
61
    'C' : 16,
62
    'R' : 8,
63
    'W' : 4,
64
    'E' : 2,
65
    'F' : 1
66
    }
67

    
68
DEPRECATED_ALIASES = {
69
    # New name, deprecated name.
70
    'repr': 'backquote',
71
    'expr': 'discard',
72
    'assignname': 'assname',
73
    'assignattr': 'assattr',
74
    'attribute': 'getattr',
75
    'call': 'callfunc',
76
    'importfrom': 'from',
77
    'classdef': 'class',
78
    'functiondef': 'function',
79
    'generatorexp': 'genexpr',
80
}
81

    
82
_MSG_ORDER = 'EWRCIF'
83
MSG_STATE_SCOPE_CONFIG = 0
84
MSG_STATE_SCOPE_MODULE = 1
85
MSG_STATE_CONFIDENCE = 2
86

    
87
# Allow stopping after the first semicolon encountered,
88
# so that an option can be continued with the reasons
89
# why it is active or disabled.
90
OPTION_RGX = re.compile(r'\s*#.*\bpylint:\s*([^;]+);{0,1}')
91

    
92
# The line/node distinction does not apply to fatal errors and reports.
93
_SCOPE_EXEMPT = 'FR'
94

    
95
class WarningScope(object):
96
    LINE = 'line-based-msg'
97
    NODE = 'node-based-msg'
98

    
99
_MsgBase = collections.namedtuple(
100
    '_MsgBase',
101
    ['msg_id', 'symbol', 'msg', 'C', 'category', 'confidence',
102
     'abspath', 'path', 'module', 'obj', 'line', 'column'])
103

    
104

    
105
class Message(_MsgBase):
106
    """This class represent a message to be issued by the reporters"""
107
    def __new__(cls, msg_id, symbol, location, msg, confidence):
108
        return _MsgBase.__new__(
109
            cls, msg_id, symbol, msg, msg_id[0], MSG_TYPES[msg_id[0]],
110
            confidence, *location)
111

    
112
    def format(self, template):
113
        """Format the message according to the given template.
114

115
        The template format is the one of the format method :
116
        cf. http://docs.python.org/2/library/string.html#formatstrings
117
        """
118
        # For some reason, _asdict on derived namedtuples does not work with
119
        # Python 3.4. Needs some investigation.
120
        return template.format(**dict(zip(self._fields, self)))
121

    
122

    
123
def get_module_and_frameid(node):
124
    """return the module name and the frame id in the module"""
125
    frame = node.frame()
126
    module, obj = '', []
127
    while frame:
128
        if isinstance(frame, Module):
129
            module = frame.name
130
        else:
131
            obj.append(getattr(frame, 'name', '<lambda>'))
132
        try:
133
            frame = frame.parent.frame()
134
        except AttributeError:
135
            frame = None
136
    obj.reverse()
137
    return module, '.'.join(obj)
138

    
139
def category_id(cid):
140
    cid = cid.upper()
141
    if cid in MSG_TYPES:
142
        return cid
143
    return MSG_TYPES_LONG.get(cid)
144

    
145

    
146
def _decoding_readline(stream, encoding):
147
    return lambda: stream.readline().decode(encoding, 'replace')
148

    
149

    
150
def tokenize_module(module):
151
    with module.stream() as stream:
152
        readline = stream.readline
153
        if sys.version_info < (3, 0):
154
            if module.file_encoding is not None:
155
                readline = _decoding_readline(stream, module.file_encoding)
156

    
157
            return list(tokenize.generate_tokens(readline))
158
        return list(tokenize.tokenize(readline))
159

    
160
def build_message_def(checker, msgid, msg_tuple):
161
    if implements(checker, (IRawChecker, ITokenChecker)):
162
        default_scope = WarningScope.LINE
163
    else:
164
        default_scope = WarningScope.NODE
165
    options = {}
166
    if len(msg_tuple) > 3:
167
        (msg, symbol, descr, options) = msg_tuple
168
    elif len(msg_tuple) > 2:
169
        (msg, symbol, descr) = msg_tuple[:3]
170
    else:
171
        # messages should have a symbol, but for backward compatibility
172
        # they may not.
173
        (msg, descr) = msg_tuple
174
        warnings.warn("[pylint 0.26] description of message %s doesn't include "
175
                      "a symbolic name" % msgid, DeprecationWarning)
176
        symbol = None
177
    options.setdefault('scope', default_scope)
178
    return MessageDefinition(checker, msgid, msg, descr, symbol, **options)
179

    
180

    
181
class MessageDefinition(object):
182
    def __init__(self, checker, msgid, msg, descr, symbol, scope,
183
                 minversion=None, maxversion=None, old_names=None):
184
        self.checker = checker
185
        assert len(msgid) == 5, 'Invalid message id %s' % msgid
186
        assert msgid[0] in MSG_TYPES, \
187
               'Bad message type %s in %r' % (msgid[0], msgid)
188
        self.msgid = msgid
189
        self.msg = msg
190
        self.descr = descr
191
        self.symbol = symbol
192
        self.scope = scope
193
        self.minversion = minversion
194
        self.maxversion = maxversion
195
        self.old_names = old_names or []
196

    
197
    def may_be_emitted(self):
198
        """return True if message may be emitted using the current interpreter"""
199
        if self.minversion is not None and self.minversion > sys.version_info:
200
            return False
201
        if self.maxversion is not None and self.maxversion <= sys.version_info:
202
            return False
203
        return True
204

    
205
    def format_help(self, checkerref=False):
206
        """return the help string for the given message id"""
207
        desc = self.descr
208
        if checkerref:
209
            desc += ' This message belongs to the %s checker.' % \
210
                   self.checker.name
211
        title = self.msg
212
        if self.symbol:
213
            msgid = '%s (%s)' % (self.symbol, self.msgid)
214
        else:
215
            msgid = self.msgid
216
        if self.minversion or self.maxversion:
217
            restr = []
218
            if self.minversion:
219
                restr.append('< %s' % '.'.join([str(n) for n in self.minversion]))
220
            if self.maxversion:
221
                restr.append('>= %s' % '.'.join([str(n) for n in self.maxversion]))
222
            restr = ' or '.join(restr)
223
            if checkerref:
224
                desc += " It can't be emitted when using Python %s." % restr
225
            else:
226
                desc += " This message can't be emitted when using Python %s." % restr
227
        desc = _normalize_text(' '.join(desc.split()), indent='  ')
228
        if title != '%s':
229
            title = title.splitlines()[0]
230
            return ':%s: *%s*\n%s' % (msgid, title, desc)
231
        return ':%s:\n%s' % (msgid, desc)
232

    
233

    
234
class MessagesHandlerMixIn(object):
235
    """a mix-in class containing all the messages related methods for the main
236
    lint class
237
    """
238

    
239
    def __init__(self):
240
        self._msgs_state = {}
241
        self.msg_status = 0
242

    
243
    def _checker_messages(self, checker):
244
        for checker in self._checkers[checker.lower()]:
245
            for msgid in checker.msgs:
246
                yield msgid
247

    
248
    def disable(self, msgid, scope='package', line=None, ignore_unknown=False):
249
        """don't output message of the given id"""
250
        assert scope in ('package', 'module')
251
        # handle disable=all by disabling all categories
252
        if msgid == 'all':
253
            for msgid in MSG_TYPES:
254
                self.disable(msgid, scope, line)
255
            return
256
        # msgid is a category?
257
        catid = category_id(msgid)
258
        if catid is not None:
259
            for _msgid in self.msgs_store._msgs_by_category.get(catid):
260
                self.disable(_msgid, scope, line)
261
            return
262
        # msgid is a checker name?
263
        if msgid.lower() in self._checkers:
264
            msgs_store = self.msgs_store
265
            for checker in self._checkers[msgid.lower()]:
266
                for _msgid in checker.msgs:
267
                    if _msgid in msgs_store._alternative_names:
268
                        self.disable(_msgid, scope, line)
269
            return
270
        # msgid is report id?
271
        if msgid.lower().startswith('rp'):
272
            self.disable_report(msgid)
273
            return
274

    
275
        try:
276
            # msgid is a symbolic or numeric msgid.
277
            msg = self.msgs_store.check_message_id(msgid)
278
        except UnknownMessage:
279
            if ignore_unknown:
280
                return
281
            raise
282

    
283
        if scope == 'module':
284
            self.file_state.set_msg_status(msg, line, False)
285
            if msg.symbol != 'locally-disabled':
286
                self.add_message('locally-disabled', line=line,
287
                                 args=(msg.symbol, msg.msgid))
288

    
289
        else:
290
            msgs = self._msgs_state
291
            msgs[msg.msgid] = False
292
            # sync configuration object
293
            self.config.disable = [self._message_symbol(mid)
294
                                   for mid, val in six.iteritems(msgs)
295
                                   if not val]
296

    
297
    def _message_symbol(self, msgid):
298
        """Get the message symbol of the given message id
299

300
        Return the original message id if the message does not
301
        exist.
302
        """
303
        try:
304
            return self.msgs_store.check_message_id(msgid).symbol
305
        except UnknownMessage:
306
            return msgid
307

    
308
    def enable(self, msgid, scope='package', line=None, ignore_unknown=False):
309
        """reenable message of the given id"""
310
        assert scope in ('package', 'module')
311
        if msgid == 'all':
312
            for msgid_ in MSG_TYPES:
313
                self.enable(msgid_, scope=scope, line=line)
314
            if not self._python3_porting_mode:
315
                # Don't activate the python 3 porting checker if it
316
                # wasn't activated explicitly.
317
                self.disable('python3')
318
            return
319
        catid = category_id(msgid)
320
        # msgid is a category?
321
        if catid is not None:
322
            for msgid in self.msgs_store._msgs_by_category.get(catid):
323
                self.enable(msgid, scope, line)
324
            return
325
        # msgid is a checker name?
326
        if msgid.lower() in self._checkers:
327
            for checker in self._checkers[msgid.lower()]:
328
                for msgid_ in checker.msgs:
329
                    self.enable(msgid_, scope, line)
330
            return
331
        # msgid is report id?
332
        if msgid.lower().startswith('rp'):
333
            self.enable_report(msgid)
334
            return
335

    
336
        try:
337
            # msgid is a symbolic or numeric msgid.
338
            msg = self.msgs_store.check_message_id(msgid)
339
        except UnknownMessage:
340
            if ignore_unknown:
341
                return
342
            raise
343

    
344
        if scope == 'module':
345
            self.file_state.set_msg_status(msg, line, True)
346
            self.add_message('locally-enabled', line=line, args=(msg.symbol, msg.msgid))
347
        else:
348
            msgs = self._msgs_state
349
            msgs[msg.msgid] = True
350
            # sync configuration object
351
            self.config.enable = [mid for mid, val in six.iteritems(msgs) if val]
352

    
353
    def get_message_state_scope(self, msgid, line=None, confidence=UNDEFINED):
354
        """Returns the scope at which a message was enabled/disabled."""
355
        if self.config.confidence and confidence.name not in self.config.confidence:
356
            return MSG_STATE_CONFIDENCE
357
        try:
358
            if line in self.file_state._module_msgs_state[msgid]:
359
                return MSG_STATE_SCOPE_MODULE
360
        except (KeyError, TypeError):
361
            return MSG_STATE_SCOPE_CONFIG
362

    
363
    def is_message_enabled(self, msg_descr, line=None, confidence=None):
364
        """return true if the message associated to the given message id is
365
        enabled
366

367
        msgid may be either a numeric or symbolic message id.
368
        """
369
        if self.config.confidence and confidence:
370
            if confidence.name not in self.config.confidence:
371
                return False
372
        try:
373
            msgid = self.msgs_store.check_message_id(msg_descr).msgid
374
        except UnknownMessage:
375
            # The linter checks for messages that are not registered
376
            # due to version mismatch, just treat them as message IDs
377
            # for now.
378
            msgid = msg_descr
379
        if line is None:
380
            return self._msgs_state.get(msgid, True)
381
        try:
382
            return self.file_state._module_msgs_state[msgid][line]
383
        except KeyError:
384
            return self._msgs_state.get(msgid, True)
385

    
386
    def add_message(self, msg_descr, line=None, node=None, args=None, confidence=UNDEFINED):
387
        """Adds a message given by ID or name.
388

389
        If provided, the message string is expanded using args
390

391
        AST checkers should must the node argument (but may optionally
392
        provide line if the line number is different), raw and token checkers
393
        must provide the line argument.
394
        """
395
        msg_info = self.msgs_store.check_message_id(msg_descr)
396
        msgid = msg_info.msgid
397
        # backward compatibility, message may not have a symbol
398
        symbol = msg_info.symbol or msgid
399
        # Fatal messages and reports are special, the node/scope distinction
400
        # does not apply to them.
401
        if msgid[0] not in _SCOPE_EXEMPT:
402
            if msg_info.scope == WarningScope.LINE:
403
                assert node is None and line is not None, (
404
                    'Message %s must only provide line, got line=%s, node=%s' % (msgid, line, node))
405
            elif msg_info.scope == WarningScope.NODE:
406
                # Node-based warnings may provide an override line.
407
                assert node is not None, 'Message %s must provide Node, got None'
408

    
409
        if line is None and node is not None:
410
            line = node.fromlineno
411
        if hasattr(node, 'col_offset'):
412
            col_offset = node.col_offset # XXX measured in bytes for utf-8, divide by two for chars?
413
        else:
414
            col_offset = None
415
        # should this message be displayed
416
        if not self.is_message_enabled(msgid, line, confidence):
417
            self.file_state.handle_ignored_message(
418
                self.get_message_state_scope(msgid, line, confidence),
419
                msgid, line, node, args, confidence)
420
            return
421
        # update stats
422
        msg_cat = MSG_TYPES[msgid[0]]
423
        self.msg_status |= MSG_TYPES_STATUS[msgid[0]]
424
        self.stats[msg_cat] += 1
425
        self.stats['by_module'][self.current_name][msg_cat] += 1
426
        try:
427
            self.stats['by_msg'][symbol] += 1
428
        except KeyError:
429
            self.stats['by_msg'][symbol] = 1
430
        # expand message ?
431
        msg = msg_info.msg
432
        if args:
433
            msg %= args
434
        # get module and object
435
        if node is None:
436
            module, obj = self.current_name, ''
437
            abspath = self.current_file
438
        else:
439
            module, obj = get_module_and_frameid(node)
440
            abspath = node.root().file
441
        path = abspath.replace(self.reporter.path_strip_prefix, '')
442
        # add the message
443
        self.reporter.handle_message(
444
            Message(msgid, symbol,
445
                    (abspath, path, module, obj, line or 1, col_offset or 0), msg, confidence))
446

    
447
    def print_full_documentation(self):
448
        """output a full documentation in ReST format"""
449
        print("Pylint global options and switches")
450
        print("----------------------------------")
451
        print("")
452
        print("Pylint provides global options and switches.")
453
        print("")
454

    
455
        by_checker = {}
456
        for checker in self.get_checkers():
457
            if checker.name == 'master':
458
                if checker.options:
459
                    for section, options in checker.options_by_section():
460
                        if section is None:
461
                            title = 'General options'
462
                        else:
463
                            title = '%s options' % section.capitalize()
464
                        print(title)
465
                        print('~' * len(title))
466
                        _rest_format_section(sys.stdout, None, options)
467
                        print("")
468
            else:
469
                try:
470
                    by_checker[checker.name][0] += checker.options_and_values()
471
                    by_checker[checker.name][1].update(checker.msgs)
472
                    by_checker[checker.name][2] += checker.reports
473
                except KeyError:
474
                    by_checker[checker.name] = [list(checker.options_and_values()),
475
                                                dict(checker.msgs),
476
                                                list(checker.reports)]
477

    
478
        print("Pylint checkers' options and switches")
479
        print("-------------------------------------")
480
        print("")
481
        print("Pylint checkers can provide three set of features:")
482
        print("")
483
        print("* options that control their execution,")
484
        print("* messages that they can raise,")
485
        print("* reports that they can generate.")
486
        print("")
487
        print("Below is a list of all checkers and their features.")
488
        print("")
489

    
490
        for checker, (options, msgs, reports) in six.iteritems(by_checker):
491
            title = '%s checker' % (checker.replace("_", " ").title())
492
            print(title)
493
            print('~' * len(title))
494
            print("")
495
            print("Verbatim name of the checker is ``%s``." % checker)
496
            print("")
497
            if options:
498
                title = 'Options'
499
                print(title)
500
                print('^' * len(title))
501
                _rest_format_section(sys.stdout, None, options)
502
                print("")
503
            if msgs:
504
                title = 'Messages'
505
                print(title)
506
                print('~' * len(title))
507
                for msgid, msg in sorted(six.iteritems(msgs),
508
                                         key=lambda kv: (_MSG_ORDER.index(kv[0][0]), kv[1])):
509
                    msg = build_message_def(checker, msgid, msg)
510
                    print(msg.format_help(checkerref=False))
511
                print("")
512
            if reports:
513
                title = 'Reports'
514
                print(title)
515
                print('~' * len(title))
516
                for report in reports:
517
                    print(':%s: %s' % report[:2])
518
                print("")
519
            print("")
520

    
521

    
522
class FileState(object):
523
    """Hold internal state specific to the currently analyzed file"""
524

    
525
    def __init__(self, modname=None):
526
        self.base_name = modname
527
        self._module_msgs_state = {}
528
        self._raw_module_msgs_state = {}
529
        self._ignored_msgs = collections.defaultdict(set)
530
        self._suppression_mapping = {}
531

    
532
    def collect_block_lines(self, msgs_store, module_node):
533
        """Walk the AST to collect block level options line numbers."""
534
        for msg, lines in six.iteritems(self._module_msgs_state):
535
            self._raw_module_msgs_state[msg] = lines.copy()
536
        orig_state = self._module_msgs_state.copy()
537
        self._module_msgs_state = {}
538
        self._suppression_mapping = {}
539
        self._collect_block_lines(msgs_store, module_node, orig_state)
540

    
541
    def _collect_block_lines(self, msgs_store, node, msg_state):
542
        """Recursivly walk (depth first) AST to collect block level options line
543
        numbers.
544
        """
545
        for child in node.get_children():
546
            self._collect_block_lines(msgs_store, child, msg_state)
547
        first = node.fromlineno
548
        last = node.tolineno
549
        # first child line number used to distinguish between disable
550
        # which are the first child of scoped node with those defined later.
551
        # For instance in the code below:
552
        #
553
        # 1.   def meth8(self):
554
        # 2.        """test late disabling"""
555
        # 3.        # pylint: disable=E1102
556
        # 4.        print self.blip
557
        # 5.        # pylint: disable=E1101
558
        # 6.        print self.bla
559
        #
560
        # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6
561
        #
562
        # this is necessary to disable locally messages applying to class /
563
        # function using their fromlineno
564
        if (isinstance(node, (nodes.Module, nodes.ClassDef, nodes.FunctionDef))
565
                and node.body):
566
            firstchildlineno = node.body[0].fromlineno
567
        else:
568
            firstchildlineno = last
569
        for msgid, lines in six.iteritems(msg_state):
570
            for lineno, state in list(lines.items()):
571
                original_lineno = lineno
572
                if first > lineno or last < lineno:
573
                    continue
574
                # Set state for all lines for this block, if the
575
                # warning is applied to nodes.
576
                if  msgs_store.check_message_id(msgid).scope == WarningScope.NODE:
577
                    if lineno > firstchildlineno:
578
                        state = True
579
                    first_, last_ = node.block_range(lineno)
580
                else:
581
                    first_ = lineno
582
                    last_ = last
583
                for line in range(first_, last_+1):
584
                    # do not override existing entries
585
                    if line in self._module_msgs_state.get(msgid, ()):
586
                        continue
587
                    if line in lines: # state change in the same block
588
                        state = lines[line]
589
                        original_lineno = line
590
                    if not state:
591
                        self._suppression_mapping[(msgid, line)] = original_lineno
592
                    try:
593
                        self._module_msgs_state[msgid][line] = state
594
                    except KeyError:
595
                        self._module_msgs_state[msgid] = {line: state}
596
                del lines[lineno]
597

    
598
    def set_msg_status(self, msg, line, status):
599
        """Set status (enabled/disable) for a given message at a given line"""
600
        assert line > 0
601
        try:
602
            self._module_msgs_state[msg.msgid][line] = status
603
        except KeyError:
604
            self._module_msgs_state[msg.msgid] = {line: status}
605

    
606
    def handle_ignored_message(self, state_scope, msgid, line,
607
                               node, args, confidence): # pylint: disable=unused-argument
608
        """Report an ignored message.
609

610
        state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG,
611
        depending on whether the message was disabled locally in the module,
612
        or globally. The other arguments are the same as for add_message.
613
        """
614
        if state_scope == MSG_STATE_SCOPE_MODULE:
615
            try:
616
                orig_line = self._suppression_mapping[(msgid, line)]
617
                self._ignored_msgs[(msgid, orig_line)].add(line)
618
            except KeyError:
619
                pass
620

    
621
    def iter_spurious_suppression_messages(self, msgs_store):
622
        for warning, lines in six.iteritems(self._raw_module_msgs_state):
623
            for line, enable in six.iteritems(lines):
624
                if not enable and (warning, line) not in self._ignored_msgs:
625
                    yield 'useless-suppression', line, \
626
                        (msgs_store.get_msg_display_string(warning),)
627
        # don't use iteritems here, _ignored_msgs may be modified by add_message
628
        for (warning, from_), lines in list(self._ignored_msgs.items()):
629
            for line in lines:
630
                yield 'suppressed-message', line, \
631
                    (msgs_store.get_msg_display_string(warning), from_)
632

    
633

    
634
class MessagesStore(object):
635
    """The messages store knows information about every possible message but has
636
    no particular state during analysis.
637
    """
638

    
639
    def __init__(self):
640
        # Primary registry for all active messages (i.e. all messages
641
        # that can be emitted by pylint for the underlying Python
642
        # version). It contains the 1:1 mapping from symbolic names
643
        # to message definition objects.
644
        self._messages = {}
645
        # Maps alternative names (numeric IDs, deprecated names) to
646
        # message definitions. May contain several names for each definition
647
        # object.
648
        self._alternative_names = {}
649
        self._msgs_by_category = collections.defaultdict(list)
650

    
651
    @property
652
    def messages(self):
653
        """The list of all active messages."""
654
        return six.itervalues(self._messages)
655

    
656
    def add_renamed_message(self, old_id, old_symbol, new_symbol):
657
        """Register the old ID and symbol for a warning that was renamed.
658

659
        This allows users to keep using the old ID/symbol in suppressions.
660
        """
661
        msg = self.check_message_id(new_symbol)
662
        msg.old_names.append((old_id, old_symbol))
663
        self._alternative_names[old_id] = msg
664
        self._alternative_names[old_symbol] = msg
665

    
666
    def register_messages(self, checker):
667
        """register a dictionary of messages
668

669
        Keys are message ids, values are a 2-uple with the message type and the
670
        message itself
671

672
        message ids should be a string of len 4, where the two first characters
673
        are the checker id and the two last the message id in this checker
674
        """
675
        chkid = None
676
        for msgid, msg_tuple in six.iteritems(checker.msgs):
677
            msg = build_message_def(checker, msgid, msg_tuple)
678
            assert msg.symbol not in self._messages, \
679
                    'Message symbol %r is already defined' % msg.symbol
680
            # avoid duplicate / malformed ids
681
            assert msg.msgid not in self._alternative_names, \
682
                   'Message id %r is already defined' % msgid
683
            assert chkid is None or chkid == msg.msgid[1:3], \
684
                   'Inconsistent checker part in message id %r' % msgid
685
            chkid = msg.msgid[1:3]
686
            self._messages[msg.symbol] = msg
687
            self._alternative_names[msg.msgid] = msg
688
            for old_id, old_symbol in msg.old_names:
689
                self._alternative_names[old_id] = msg
690
                self._alternative_names[old_symbol] = msg
691
            self._msgs_by_category[msg.msgid[0]].append(msg.msgid)
692

    
693
    def check_message_id(self, msgid):
694
        """returns the Message object for this message.
695

696
        msgid may be either a numeric or symbolic id.
697

698
        Raises UnknownMessage if the message id is not defined.
699
        """
700
        if msgid[1:].isdigit():
701
            msgid = msgid.upper()
702
        for source in (self._alternative_names, self._messages):
703
            try:
704
                return source[msgid]
705
            except KeyError:
706
                pass
707
        raise UnknownMessage('No such message id %s' % msgid)
708

    
709
    def get_msg_display_string(self, msgid):
710
        """Generates a user-consumable representation of a message.
711

712
        Can be just the message ID or the ID and the symbol.
713
        """
714
        return repr(self.check_message_id(msgid).symbol)
715

    
716
    def help_message(self, msgids):
717
        """display help messages for the given message identifiers"""
718
        for msgid in msgids:
719
            try:
720
                print(self.check_message_id(msgid).format_help(checkerref=True))
721
                print("")
722
            except UnknownMessage as ex:
723
                print(ex)
724
                print("")
725
                continue
726

    
727
    def list_messages(self):
728
        """output full messages list documentation in ReST format"""
729
        msgs = sorted(six.itervalues(self._messages), key=lambda msg: msg.msgid)
730
        for msg in msgs:
731
            if not msg.may_be_emitted():
732
                continue
733
            print(msg.format_help(checkerref=False))
734
        print("")
735

    
736

    
737
class ReportsHandlerMixIn(object):
738
    """a mix-in class containing all the reports and stats manipulation
739
    related methods for the main lint class
740
    """
741
    def __init__(self):
742
        self._reports = collections.defaultdict(list)
743
        self._reports_state = {}
744

    
745
    def report_order(self):
746
        """ Return a list of reports, sorted in the order
747
        in which they must be called.
748
        """
749
        return list(self._reports)
750

    
751
    def register_report(self, reportid, r_title, r_cb, checker):
752
        """register a report
753

754
        reportid is the unique identifier for the report
755
        r_title the report's title
756
        r_cb the method to call to make the report
757
        checker is the checker defining the report
758
        """
759
        reportid = reportid.upper()
760
        self._reports[checker].append((reportid, r_title, r_cb))
761

    
762
    def enable_report(self, reportid):
763
        """disable the report of the given id"""
764
        reportid = reportid.upper()
765
        self._reports_state[reportid] = True
766

    
767
    def disable_report(self, reportid):
768
        """disable the report of the given id"""
769
        reportid = reportid.upper()
770
        self._reports_state[reportid] = False
771

    
772
    def report_is_enabled(self, reportid):
773
        """return true if the report associated to the given identifier is
774
        enabled
775
        """
776
        return self._reports_state.get(reportid, True)
777

    
778
    def make_reports(self, stats, old_stats):
779
        """render registered reports"""
780
        sect = Section('Report',
781
                       '%s statements analysed.'% (self.stats['statement']))
782
        for checker in self.report_order():
783
            for reportid, r_title, r_cb in self._reports[checker]:
784
                if not self.report_is_enabled(reportid):
785
                    continue
786
                report_sect = Section(r_title)
787
                try:
788
                    r_cb(report_sect, stats, old_stats)
789
                except EmptyReport:
790
                    continue
791
                report_sect.report_id = reportid
792
                sect.append(report_sect)
793
        return sect
794

    
795
    def add_stats(self, **kwargs):
796
        """add some stats entries to the statistic dictionary
797
        raise an AssertionError if there is a key conflict
798
        """
799
        for key, value in six.iteritems(kwargs):
800
            if key[-1] == '_':
801
                key = key[:-1]
802
            assert key not in self.stats
803
            self.stats[key] = value
804
        return self.stats
805

    
806

    
807
def expand_modules(files_or_modules, black_list):
808
    """take a list of files/modules/packages and return the list of tuple
809
    (file, module name) which have to be actually checked
810
    """
811
    result = []
812
    errors = []
813
    for something in files_or_modules:
814
        if exists(something):
815
            # this is a file or a directory
816
            try:
817
                modname = '.'.join(modpath_from_file(something))
818
            except ImportError:
819
                modname = splitext(basename(something))[0]
820
            if isdir(something):
821
                filepath = join(something, '__init__.py')
822
            else:
823
                filepath = something
824
        else:
825
            # suppose it's a module or package
826
            modname = something
827
            try:
828
                filepath = file_from_modpath(modname.split('.'))
829
                if filepath is None:
830
                    continue
831
            except (ImportError, SyntaxError) as ex:
832
                # FIXME p3k : the SyntaxError is a Python bug and should be
833
                # removed as soon as possible http://bugs.python.org/issue10588
834
                errors.append({'key': 'fatal', 'mod': modname, 'ex': ex})
835
                continue
836
        if getattr(filepath,"copy",None)==None:
837
          filepath = normpath(filepath)
838
        else:
839
          filepath = filepath.copy(normpath(filepath))
840
        result.append({'path': filepath, 'name': modname, 'isarg': True,
841
                       'basepath': filepath, 'basename': modname})
842
        if not (modname.endswith('.__init__') or modname == '__init__') \
843
                and '__init__.py' in filepath:
844
            for subfilepath in get_module_files(dirname(filepath), black_list):
845
                if filepath == subfilepath:
846
                    continue
847
                submodname = '.'.join(modpath_from_file(subfilepath))
848
                result.append({'path': subfilepath, 'name': submodname,
849
                               'isarg': False,
850
                               'basepath': filepath, 'basename': modname})
851
    return result, errors
852

    
853

    
854
class PyLintASTWalker(object):
855

    
856
    def __init__(self, linter):
857
        # callbacks per node types
858
        self.nbstatements = 0
859
        self.visit_events = collections.defaultdict(list)
860
        self.leave_events = collections.defaultdict(list)
861
        self.linter = linter
862

    
863
    def _is_method_enabled(self, method):
864
        if not hasattr(method, 'checks_msgs'):
865
            return True
866
        for msg_desc in method.checks_msgs:
867
            if self.linter.is_message_enabled(msg_desc):
868
                return True
869
        return False
870

    
871
    def add_checker(self, checker):
872
        """walk to the checker's dir and collect visit and leave methods"""
873
        # XXX : should be possible to merge needed_checkers and add_checker
874
        vcids = set()
875
        lcids = set()
876
        visits = self.visit_events
877
        leaves = self.leave_events
878
        for member in dir(checker):
879
            cid = member[6:]
880
            if cid == 'default':
881
                continue
882
            if member.startswith('visit_'):
883
                v_meth = getattr(checker, member)
884
                # don't use visit_methods with no activated message:
885
                if self._is_method_enabled(v_meth):
886
                    visits[cid].append(v_meth)
887
                    vcids.add(cid)
888
            elif member.startswith('leave_'):
889
                l_meth = getattr(checker, member)
890
                # don't use leave_methods with no activated message:
891
                if self._is_method_enabled(l_meth):
892
                    leaves[cid].append(l_meth)
893
                    lcids.add(cid)
894
        visit_default = getattr(checker, 'visit_default', None)
895
        if visit_default:
896
            for cls in nodes.ALL_NODE_CLASSES:
897
                cid = cls.__name__.lower()
898
                if cid not in vcids:
899
                    visits[cid].append(visit_default)
900
        # for now we have no "leave_default" method in Pylint
901

    
902
    def walk(self, astroid):
903
        """call visit events of astroid checkers for the given node, recurse on
904
        its children, then leave events.
905
        """
906
        cid = astroid.__class__.__name__.lower()
907

    
908
        # Detect if the node is a new name for a deprecated alias.
909
        # In this case, favour the methods for the deprecated
910
        # alias if any,  in order to maintain backwards
911
        # compatibility.
912
        old_cid = DEPRECATED_ALIASES.get(cid)
913
        visit_events = ()
914
        leave_events = ()
915

    
916
        if old_cid:
917
            visit_events = self.visit_events.get(old_cid, ())
918
            leave_events = self.leave_events.get(old_cid, ())
919
            if visit_events or leave_events:
920
                msg = ("Implemented method {meth}_{old} instead of {meth}_{new}. "
921
                       "This will be supported until Pylint 2.0.")
922
                if visit_events:
923
                    warnings.warn(msg.format(meth="visit", old=old_cid, new=cid),
924
                                  PendingDeprecationWarning)
925
                if leave_events:
926
                    warnings.warn(msg.format(meth="leave", old=old_cid, new=cid),
927
                                  PendingDeprecationWarning)
928

    
929
        visit_events = itertools.chain(visit_events,
930
                                       self.visit_events.get(cid, ()))
931
        leave_events = itertools.chain(leave_events,
932
                                       self.leave_events.get(cid, ()))
933

    
934
        if astroid.is_statement:
935
            self.nbstatements += 1
936
        # generate events for this node on each checker
937
        for cb in visit_events or ():
938
            cb(astroid)
939
        # recurse on children
940
        for child in astroid.get_children():
941
            self.walk(child)
942
        for cb in leave_events or ():
943
            cb(astroid)
944

    
945

    
946
PY_EXTS = ('.py', '.pyc', '.pyo', '.pyw', '.so', '.dll')
947

    
948
def register_plugins(linter, directory):
949
    """load all module and package in the given directory, looking for a
950
    'register' function in each one, used to register pylint checkers
951
    """
952
    imported = {}
953
    for filename in os.listdir(directory):
954
        base, extension = splitext(filename)
955
        if base in imported or base == '__pycache__':
956
            continue
957
        if extension in PY_EXTS and base != '__init__' or (
958
                not extension and isdir(join(directory, base))):
959
            try:
960
                module = load_module_from_file(join(directory, filename))
961
            except ValueError:
962
                # empty module name (usually emacs auto-save files)
963
                continue
964
            except ImportError as exc:
965
                print("Problem importing module %s: %s" % (filename, exc),
966
                      file=sys.stderr)
967
            else:
968
                if hasattr(module, 'register'):
969
                    module.register(linter)
970
                    imported[base] = 1
971

    
972
def get_global_option(checker, option, default=None):
973
    """ Retrieve an option defined by the given *checker* or
974
    by all known option providers.
975

976
    It will look in the list of all options providers
977
    until the given *option* will be found.
978
    If the option wasn't found, the *default* value will be returned.
979
    """
980
    # First, try in the given checker's config.
981
    # After that, look in the options providers.
982

    
983
    try:
984
        return getattr(checker.config, option.replace("-", "_"))
985
    except AttributeError:
986
        pass
987
    for provider in checker.linter.options_providers:
988
        for options in provider.options:
989
            if options[0] == option:
990
                return getattr(provider.config, option.replace("-", "_"))
991
    return default
992

    
993

    
994
def deprecated_option(shortname=None, opt_type=None, help_msg=None):
995
    def _warn_deprecated(option, optname, *args): # pylint: disable=unused-argument
996
        msg = ("Warning: option %s is obsolete and "
997
               "it is slated for removal in Pylint 1.6.\n")
998
        sys.stderr.write(msg % (optname,))
999

    
1000
    option = {
1001
        'help': help_msg,
1002
        'hide': True,
1003
        'type': opt_type,
1004
        'action': 'callback',
1005
        'callback': _warn_deprecated,
1006
        'deprecated': True
1007
    }
1008
    if shortname:
1009
        option['shortname'] = shortname
1010
    return option
1011

    
1012

    
1013
def _splitstrip(string, sep=','):
1014
    """return a list of stripped string by splitting the string given as
1015
    argument on `sep` (',' by default). Empty string are discarded.
1016

1017
    >>> _splitstrip('a, b, c   ,  4,,')
1018
    ['a', 'b', 'c', '4']
1019
    >>> _splitstrip('a')
1020
    ['a']
1021
    >>>
1022

1023
    :type string: str or unicode
1024
    :param string: a csv line
1025

1026
    :type sep: str or unicode
1027
    :param sep: field separator, default to the comma (',')
1028

1029
    :rtype: str or unicode
1030
    :return: the unquoted string (or the input string if it wasn't quoted)
1031
    """
1032
    return [word.strip() for word in string.split(sep) if word.strip()]
1033

    
1034

    
1035
def _unquote(string):
1036
    """remove optional quotes (simple or double) from the string
1037

1038
    :type string: str or unicode
1039
    :param string: an optionally quoted string
1040

1041
    :rtype: str or unicode
1042
    :return: the unquoted string (or the input string if it wasn't quoted)
1043
    """
1044
    if not string:
1045
        return string
1046
    if string[0] in '"\'':
1047
        string = string[1:]
1048
    if string[-1] in '"\'':
1049
        string = string[:-1]
1050
    return string
1051

    
1052

    
1053
def _normalize_text(text, line_len=80, indent=''):
1054
    """Wrap the text on the given line length."""
1055
    return '\n'.join(textwrap.wrap(text, width=line_len, initial_indent=indent,
1056
                                   subsequent_indent=indent))
1057

    
1058

    
1059
def _check_csv(value):
1060
    if isinstance(value, (list, tuple)):
1061
        return value
1062
    return _splitstrip(value)
1063

    
1064

    
1065
if six.PY2:
1066
    def _encode(string, encoding):
1067
        # pylint: disable=undefined-variable
1068
        if isinstance(string, unicode):
1069
            return string.encode(encoding)
1070
        return str(string)
1071
else:
1072
    def _encode(string, _):
1073
        return str(string)
1074

    
1075
def _get_encoding(encoding, stream):
1076
    encoding = encoding or getattr(stream, 'encoding', None)
1077
    if not encoding:
1078
        import locale
1079
        encoding = locale.getpreferredencoding()
1080
    return encoding
1081

    
1082

    
1083
def _comment(string):
1084
    """return string as a comment"""
1085
    lines = [line.strip() for line in string.splitlines()]
1086
    return '# ' + ('%s# ' % os.linesep).join(lines)
1087

    
1088

    
1089
def _format_option_value(optdict, value):
1090
    """return the user input's value from a 'compiled' value"""
1091
    if isinstance(value, (list, tuple)):
1092
        value = ','.join(value)
1093
    elif isinstance(value, dict):
1094
        value = ','.join('%s:%s' % (k, v) for k, v in value.items())
1095
    elif hasattr(value, 'match'): # optdict.get('type') == 'regexp'
1096
        # compiled regexp
1097
        value = value.pattern
1098
    elif optdict.get('type') == 'yn':
1099
        value = value and 'yes' or 'no'
1100
    elif isinstance(value, six.string_types) and value.isspace():
1101
        value = "'%s'" % value
1102
    return value
1103

    
1104

    
1105
def _ini_format_section(stream, section, options, encoding=None, doc=None):
1106
    """format an options section using the INI format"""
1107
    encoding = _get_encoding(encoding, stream)
1108
    if doc:
1109
        print(_encode(_comment(doc), encoding), file=stream)
1110
    print('[%s]' % section, file=stream)
1111
    _ini_format(stream, options, encoding)
1112

    
1113

    
1114
def _ini_format(stream, options, encoding):
1115
    """format options using the INI format"""
1116
    for optname, optdict, value in options:
1117
        value = _format_option_value(optdict, value)
1118
        help = optdict.get('help')
1119
        if help:
1120
            help = _normalize_text(help, line_len=79, indent='# ')
1121
            print(file=stream)
1122
            print(_encode(help, encoding), file=stream)
1123
        else:
1124
            print(file=stream)
1125
        if value is None:
1126
            print('#%s=' % optname, file=stream)
1127
        else:
1128
            value = _encode(value, encoding).strip()
1129
            print('%s=%s' % (optname, value), file=stream)
1130

    
1131
format_section = _ini_format_section
1132

    
1133

    
1134
def _rest_format_section(stream, section, options, encoding=None, doc=None):
1135
    """format an options section using as ReST formatted output"""
1136
    encoding = _get_encoding(encoding, stream)
1137
    if section:
1138
        print('%s\n%s' % (section, "'"*len(section)), file=stream)
1139
    if doc:
1140
        print(_encode(_normalize_text(doc, line_len=79, indent=''), encoding), file=stream)
1141
        print(file=stream)
1142
    for optname, optdict, value in options:
1143
        help = optdict.get('help')
1144
        print(':%s:' % optname, file=stream)
1145
        if help:
1146
            help = _normalize_text(help, line_len=79, indent='  ')
1147
            print(_encode(help, encoding), file=stream)
1148
        if value:
1149
            value = _encode(_format_option_value(optdict, value), encoding)
1150
            print(file=stream)
1151
            print('  Default: ``%s``' % value.replace("`` ", "```` ``"), file=stream)