diff options
author | Ben Gamari <ben@smart-cactus.org> | 2018-12-13 13:53:27 -0500 |
---|---|---|
committer | Ben Gamari <ben@smart-cactus.org> | 2018-12-13 21:59:20 -0500 |
commit | e9f68a1529687b11e22cf2b28e119b043dded6a6 (patch) | |
tree | 64199b60c6e966bd1bd1ba8b50f8cb35ba51bff5 /.gitlab | |
parent | 9b8713e8bf905c17251a0fad22eee690c4e50f0c (diff) | |
download | haskell-e9f68a1529687b11e22cf2b28e119b043dded6a6.tar.gz |
gitlab-ci: Add linters
These are taken from our previous arcanist linters as well as the
gitolite hooks but with some heavy refactoring.
Diffstat (limited to '.gitlab')
-rwxr-xr-x | .gitlab/linters/check-cpp.py | 22 | ||||
-rwxr-xr-x | .gitlab/linters/check-makefiles.py | 19 | ||||
-rw-r--r-- | .gitlab/linters/linter.py | 109 |
3 files changed, 150 insertions, 0 deletions
diff --git a/.gitlab/linters/check-cpp.py b/.gitlab/linters/check-cpp.py new file mode 100755 index 0000000000..144ab7dd6e --- /dev/null +++ b/.gitlab/linters/check-cpp.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# A linter to warn for ASSERT macros which are separated from their argument +# list by a space, which Clang's CPP barfs on + +from linter import run_linters, RegexpLinter + +linters = [ + RegexpLinter(r'ASSERT\s+\(', + message='CPP macros should not have a space between the macro name and their argument list'), + RegexpLinter(r'ASSERT2\s+\(', + message='CPP macros should not have a space between the macro name and their argument list'), + RegexpLinter(r'#ifdef\s+', + message='`#if defined(x)` is preferred to `#ifdef x`'), + RegexpLinter(r'#if\s+defined\s+', + message='`#if defined(x)` is preferred to `#if defined x`'), + RegexpLinter(r'#ifndef\s+', + message='`#if !defined(x)` is preferred to `#ifndef x`'), +] + +if __name__ == '__main__': + run_linters(linters) diff --git a/.gitlab/linters/check-makefiles.py b/.gitlab/linters/check-makefiles.py new file mode 100755 index 0000000000..c97838beb5 --- /dev/null +++ b/.gitlab/linters/check-makefiles.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +""" +Warn for use of `--interactive` inside Makefiles (#11468). + +Encourage the use of `$(TEST_HC_OPTS_INTERACTIVE)` instead of +`$(TEST_HC_OPTS) --interactive -ignore-dot-ghci -v0`. It's too easy to +forget one of those flags when adding a new test. +""" + +from linter import run_linters, RegexpLinter + +linters = [ + RegexpLinter(r'--interactive', + message = "Warning: Use `$(TEST_HC_OPTS_INTERACTIVE)` instead of `--interactive -ignore-dot-ghci -v0`.") +] + +if __name__ == '__main__': + run_linters(linters) #$, subdir='testsuite') diff --git a/.gitlab/linters/linter.py b/.gitlab/linters/linter.py new file mode 100644 index 0000000000..ec4f3581a8 --- /dev/null +++ b/.gitlab/linters/linter.py @@ -0,0 +1,109 @@ +""" +Utilities for linters +""" + +import os +import sys +import re +import textwrap +import subprocess +from typing import List, Optional +from collections import namedtuple + +def lint_failure(file, line_no, line_content, message): + """ 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, head_commit, + 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] + + def add_warning(self, w: Warning): + self.warnings.append(w) + + def lint(self, path): + pass + +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): + if os.path.isfile(path): + with open(path, 'r') as f: + for line_no, line in enumerate(f): + self.lint_line(path, line_no+1, line) + + def lint_line(self, path, line_no, line): + pass + +class RegexpLinter(LineLinter): + """ + A :class:`RegexpLinter` produces the given warning message for + all lines matching the given regular expression. + """ + def __init__(self, regex, message): + LineLinter.__init__(self) + self.re = re.compile(regex) + self.message = message + + def lint_line(self, path, line_no, line): + 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: List[Linter], + subdir: str = '.') -> None: + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('base', help='Base commit') + parser.add_argument('head', help='Head commit') + args = parser.parse_args() + + for path in get_changed_files(args.base, args.head, subdir): + if path.startswith('.gitlab/linters'): + continue + for linter in linters: + linter.lint(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) |