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 | |
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.
-rw-r--r-- | .circleci/images/linters/Dockerfile | 30 | ||||
-rw-r--r-- | .gitlab-ci.yml | 30 | ||||
-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 |
5 files changed, 209 insertions, 1 deletions
diff --git a/.circleci/images/linters/Dockerfile b/.circleci/images/linters/Dockerfile new file mode 100644 index 0000000000..cd9aa30e69 --- /dev/null +++ b/.circleci/images/linters/Dockerfile @@ -0,0 +1,30 @@ +FROM debian:stretch + +ENV LANG C.UTF-8 + +RUN apt-get update -qq; apt-get install -qy gnupg sudo git python3 + +RUN echo 'deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main' > /etc/apt/sources.list.d/ghc.list +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F6F88286 +RUN apt-get update -qq + +# Basic Haskell toolchain +RUN apt-get install -qy cabal-install-2.2 ghc-8.4.2 +ENV PATH /home/ghc/.local/bin:/opt/cabal/2.2/bin:/opt/ghc/8.4.2/bin:$PATH + +# Create a normal user. +RUN adduser ghc --gecos "GHC builds" --disabled-password +RUN echo "ghc ALL = NOPASSWD : ALL" > /etc/sudoers.d/ghc +USER ghc +WORKDIR /home/ghc/ + +# Build Linting tools +RUN cabal update + +RUN git clone git://github.com/haskell-infra/git-haskell-org-hooks && \ + cd git-haskell-org-hooks && \ + cabal install + +ENV PATH /home/ghc/.cabal/bin:$PATH + +CMD ["bash"] diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5af0b0c494..1c19167938 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,11 +7,34 @@ before_script: - git submodule update --init --recursive - git checkout .gitmodules +stages: + - lint + - build + +############################################################ +# Linting +############################################################ + +ghc-linters: + stage: lint + image: ghcci/linters:0.1 + script: + - | + if [ -n "$CI_MERGE_REQUEST_ID" ]; then + base="$(git merge-base $CI_MERGE_REQUEST_BRANCH_NAME HEAD)" + validate-commit-msg .git $(git rev-list $base..$CI_COMMIT_SHA) + submodchecker .git $(git rev-list $base..$CI_COMMIT_SHA) + validate-whitespace .git $(git rev-list $base..$CI_COMMIT_SHA) + .gitlab/linters/check-makefiles.py $base $CI_COMMIT_SHA + .gitlab/linters/check-cpp.py $base $CI_COMMIT_SHA + fi + ############################################################ # Validation via Pipelines (hadrian) ############################################################ .validate-hadrian: + stage: build allow_failure: true script: - bash .circleci/prepare-system.sh @@ -38,7 +61,7 @@ validate-x86_64-linux-deb8-hadrian: ############################################################ .validate: - allow_failure: true + stage: build script: - make clean || true - ./boot @@ -61,6 +84,7 @@ validate-x86_64-linux-deb8-hadrian: validate-x86_64-darwin: extends: .validate + allow_failure: true tags: - x86_64-darwin variables: @@ -120,6 +144,7 @@ validate-x86_64-linux-deb9: validate-x86_64-linux-deb9-llvm: extends: .validate-linux + allow_failure: true image: ghcci/x86_64-linux-deb9:0.2 variables: BUILD_FLAVOUR: perf-llvm @@ -144,6 +169,7 @@ validate-x86_64-linux-fedora27: validate-x86_64-linux-deb9-integer-simple: extends: .validate-linux + allow_failure: true variables: INTEGER_LIBRARY: integer-simple image: ghcci/x86_64-linux-deb9:0.2 @@ -163,6 +189,7 @@ validate-x86_64-linux-deb9-unreg: ############################################################ .validate-x86_64-windows: + stage: build variables: GHC_VERSION: "8.6.2" script: @@ -193,6 +220,7 @@ validate-x86_64-linux-deb9-unreg: ############################################################ .circleci: + stage: build image: ghcci/x86_64-linux-deb8:0.1 artifacts: when: always 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) |