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
|
"""
Utilities for linters
"""
import os
import sys
import re
import textwrap
import subprocess
from pathlib import Path
from typing import List, Optional, Callable, Sequence
from collections import namedtuple
def lint_failure(file, line_no: int, line_content: str, message: str):
""" Print a lint failure message. """
wrapper = textwrap.TextWrapper(initial_indent=' ',
subsequent_indent=' ')
body = wrapper.fill(message)
msg = '''
{file}:
|
{line_no:5d} | {line_content}
|
{body}
'''.format(file=file, line_no=line_no,
line_content=line_content,
body=body)
print(textwrap.dedent(msg))
def get_changed_files(base_commit: str, head_commit: str,
subdir: str = '.'):
""" Get the files changed by the given range of commits. """
cmd = ['git', 'diff', '--name-only',
base_commit, head_commit, '--', subdir]
files = subprocess.check_output(cmd)
return files.decode('UTF-8').split('\n')
Warning = namedtuple('Warning', 'path,line_no,line_content,message')
class Linter(object):
"""
A :class:`Linter` must implement :func:`lint`, which looks at the
given path and calls :func:`add_warning` for any lint issues found.
"""
def __init__(self):
self.warnings = [] # type: List[Warning]
self.path_filters = [] # type: List[Callable[[Path], bool]]
def add_warning(self, w: Warning):
self.warnings.append(w)
def add_path_filter(self, f: Callable[[Path], bool]) -> "Linter":
self.path_filters.append(f)
return self
def do_lint(self, path: Path):
if all(f(path) for f in self.path_filters):
self.lint(path)
def lint(self, path: Path):
raise NotImplementedError
class LineLinter(Linter):
"""
A :class:`LineLinter` must implement :func:`lint_line`, which looks at
the given line from a file and calls :func:`add_warning` for any lint
issues found.
"""
def lint(self, path: Path):
if path.is_file():
with path.open('r') as f:
for line_no, line in enumerate(f):
self.lint_line(path, line_no+1, line)
def lint_line(self, path: Path, line_no: int, line: str):
raise NotImplementedError
class RegexpLinter(LineLinter):
"""
A :class:`RegexpLinter` produces the given warning message for
all lines matching the given regular expression.
"""
def __init__(self, regex: str, message: str):
LineLinter.__init__(self)
self.re = re.compile(regex)
self.message = message
def lint_line(self, path: Path, line_no: int, line: str):
if self.re.search(line):
w = Warning(path=path, line_no=line_no, line_content=line[:-1],
message=self.message)
self.add_warning(w)
def run_linters(linters: Sequence[Linter],
subdir: str = '.') -> None:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
subparser = subparsers.add_parser('commits', help='Lint a range of commits')
subparser.add_argument('base', help='Base commit')
subparser.add_argument('head', help='Head commit')
subparser.set_defaults(get_linted_files=lambda args:
get_changed_files(args.base, args.head, subdir))
subparser = subparsers.add_parser('files', help='Lint a range of commits')
subparser.add_argument('file', nargs='+', help='File to lint')
subparser.set_defaults(get_linted_files=lambda args: args.file)
args = parser.parse_args()
linted_files = args.get_linted_files(args)
for path in linted_files:
if path.startswith('.gitlab/linters'):
continue
for linter in linters:
linter.do_lint(Path(path))
warnings = [warning
for linter in linters
for warning in linter.warnings]
warnings = sorted(warnings, key=lambda x: (x.path, x.line_no))
for w in warnings:
lint_failure(w.path, w.line_no, w.line_content, w.message)
if len(warnings) > 0:
sys.exit(1)
|