c176ca20a4f50bb2228c229c0f037397d217b1df
[ghc.git] / .gitlab / linters / linter.py
1 """
2 Utilities for linters
3 """
4
5 import os
6 import sys
7 import re
8 import textwrap
9 import subprocess
10 from pathlib import Path
11 from typing import List, Optional, Callable
12 from collections import namedtuple
13
14 def lint_failure(file, line_no, line_content, message):
15 """ Print a lint failure message. """
16 wrapper = textwrap.TextWrapper(initial_indent=' ',
17 subsequent_indent=' ')
18 body = wrapper.fill(message)
19 msg = '''
20 {file}:
21
22 |
23 {line_no:5d} | {line_content}
24 |
25
26 {body}
27 '''.format(file=file, line_no=line_no,
28 line_content=line_content,
29 body=body)
30
31 print(textwrap.dedent(msg))
32
33 def get_changed_files(base_commit, head_commit,
34 subdir: str = '.'):
35 """ Get the files changed by the given range of commits. """
36 cmd = ['git', 'diff', '--name-only',
37 base_commit, head_commit, '--', subdir]
38 files = subprocess.check_output(cmd)
39 return files.decode('UTF-8').split('\n')
40
41 Warning = namedtuple('Warning', 'path,line_no,line_content,message')
42
43 class Linter(object):
44 """
45 A :class:`Linter` must implement :func:`lint`, which looks at the
46 given path and calls :func:`add_warning` for any lint issues found.
47 """
48 def __init__(self):
49 self.warnings = [] # type: List[Warning]
50 self.path_filters = [] # type: List[Callable[[Path], bool]]
51
52 def add_warning(self, w: Warning):
53 self.warnings.append(w)
54
55 def add_path_filter(self, f: Callable[[Path], bool]) -> "Linter":
56 self.path_filters.append(f)
57 return self
58
59 def do_lint(self, path):
60 if all(f(path) for f in self.path_filters):
61 self.lint(path)
62
63 def lint(self, path):
64 raise NotImplementedError
65
66 class LineLinter(Linter):
67 """
68 A :class:`LineLinter` must implement :func:`lint_line`, which looks at
69 the given line from a file and calls :func:`add_warning` for any lint
70 issues found.
71 """
72 def lint(self, path):
73 if os.path.isfile(path):
74 with open(path, 'r') as f:
75 for line_no, line in enumerate(f):
76 self.lint_line(path, line_no+1, line)
77
78 def lint_line(self, path, line_no, line):
79 raise NotImplementedError
80
81 class RegexpLinter(LineLinter):
82 """
83 A :class:`RegexpLinter` produces the given warning message for
84 all lines matching the given regular expression.
85 """
86 def __init__(self, regex, message, path_filter=lambda path: True):
87 LineLinter.__init__(self)
88 self.re = re.compile(regex)
89 self.message = message
90 self.path_filter = path_filter
91
92 def lint_line(self, path, line_no, line):
93 if self.path_filter(path) and self.re.search(line):
94 w = Warning(path=path, line_no=line_no, line_content=line[:-1],
95 message=self.message)
96 self.add_warning(w)
97
98 def run_linters(linters: List[Linter],
99 subdir: str = '.') -> None:
100 import argparse
101 parser = argparse.ArgumentParser()
102 parser.add_argument('base', help='Base commit')
103 parser.add_argument('head', help='Head commit')
104 args = parser.parse_args()
105
106 for path in get_changed_files(args.base, args.head, subdir):
107 if path.startswith('.gitlab/linters'):
108 continue
109 for linter in linters:
110 linter.do_lint(path)
111
112 warnings = [warning
113 for linter in linters
114 for warning in linter.warnings]
115 warnings = sorted(warnings, key=lambda x: (x.path, x.line_no))
116 for w in warnings:
117 lint_failure(w.path, w.line_no, w.line_content, w.message)
118
119 if len(warnings) > 0:
120 sys.exit(1)