gitlab-ci: Add linters
authorBen Gamari <ben@smart-cactus.org>
Thu, 13 Dec 2018 18:53:27 +0000 (13:53 -0500)
committerBen Gamari <ben@smart-cactus.org>
Fri, 14 Dec 2018 02:59:20 +0000 (21:59 -0500)
These are taken from our previous arcanist linters as well as the
gitolite hooks but with some heavy refactoring.

.circleci/images/linters/Dockerfile [new file with mode: 0644]
.gitlab-ci.yml
.gitlab/linters/check-cpp.py [new file with mode: 0755]
.gitlab/linters/check-makefiles.py [new file with mode: 0755]
.gitlab/linters/linter.py [new file with mode: 0644]

diff --git a/.circleci/images/linters/Dockerfile b/.circleci/images/linters/Dockerfile
new file mode 100644 (file)
index 0000000..cd9aa30
--- /dev/null
@@ -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"]
index 5af0b0c..1c19167 100644 (file)
@@ -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 (executable)
index 0000000..144ab7d
--- /dev/null
@@ -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 (executable)
index 0000000..c97838b
--- /dev/null
@@ -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 (file)
index 0000000..ec4f358
--- /dev/null
@@ -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)