summaryrefslogtreecommitdiff
path: root/testsuite/tests
diff options
context:
space:
mode:
authorMatthew Pickering <matthewtpickering@gmail.com>2022-02-18 17:27:32 +0100
committerMarge Bot <ben+marge-bot@smart-cactus.org>2022-02-24 20:25:06 -0500
commit6555b68ca0678827b89c5624db071f5a485d18b7 (patch)
tree9dbcd231add48a179d7751606523865029d2fc1a /testsuite/tests
parent06c18990fb6f10aaf1907ba8f0fe3f1a138da159 (diff)
downloadhaskell-6555b68ca0678827b89c5624db071f5a485d18b7.tar.gz
Move linters into the tree
This MR moves the GHC linters into the tree, so that they can be run directly using Hadrian. * Query all files tracked by Git instead of using changed files, so that we can run the exact same linting step locally and in a merge request. * Only check that the changelogs don't contain TBA when RELEASE=YES. * Add hadrian/lint script, which runs all the linting steps. * Ensure the hlint job exits with a failure if hlint is not installed (otherwise we were ignoring the failure). Given that hlint doesn't seem to be available in CI at the moment, I've temporarily allowed failure in the hlint job. * Run all linting tests in CI using hadrian.
Diffstat (limited to 'testsuite/tests')
-rw-r--r--testsuite/tests/linters/Makefile91
-rw-r--r--testsuite/tests/linters/all.T25
-rw-r--r--testsuite/tests/linters/changelogs.stdout1
-rw-r--r--testsuite/tests/linters/notes.stdout48
-rwxr-xr-xtestsuite/tests/linters/regex-linters/check-changelogs.sh16
-rw-r--r--testsuite/tests/linters/regex-linters/check-cpp.py44
-rw-r--r--testsuite/tests/linters/regex-linters/check-makefiles.py45
-rwxr-xr-xtestsuite/tests/linters/regex-linters/check-version-number.sh7
-rw-r--r--testsuite/tests/linters/regex-linters/linter.py140
-rw-r--r--testsuite/tests/linters/whitespace.stdout1
10 files changed, 388 insertions, 30 deletions
diff --git a/testsuite/tests/linters/Makefile b/testsuite/tests/linters/Makefile
index 559d0bba3e..54ef4db132 100644
--- a/testsuite/tests/linters/Makefile
+++ b/testsuite/tests/linters/Makefile
@@ -1,11 +1,96 @@
TOP=../..
-ifeq "$(NOTES_UTIL)" ""
-NOTES_UTIL := $(abspath $(TOP)/../inplace/bin/notes-util)
+ifeq "$(LINT_NOTES)" ""
+LINT_NOTES := $(abspath $(TOP)/../inplace/bin/lint-notes)
+endif
+
+ifeq "$(LINT_WHITESPACE)" ""
+LINT_WHITESPACE := $(abspath $(TOP)/../inplace/bin/lint-whitespace)
endif
uniques:
python3 checkUniques/check-uniques.py $(TOP)/..
+makefiles:
+ (cd $(TOP)/tests/linters/ && python3 regex-linters/check-makefiles.py tracked)
+
+version-number:
+ regex-linters/check-version-number.sh ${TOP}/..
+
+cpp:
+ (cd $(TOP)/tests/linters/ && python3 regex-linters/check-cpp.py tracked)
+
+changelogs:
+ regex-linters/check-changelogs.sh $(TOP)/..
+
notes:
- (cd $(TOP)/.. && $(NOTES_UTIL) broken-refs)
+ (cd $(TOP)/.. && $(LINT_NOTES) broken-refs)
+
+whitespace:
+ (cd $(TOP)/.. &&\
+ $(LINT_WHITESPACE) tracked\
+ --ignore-dirs\
+ testsuite\
+ libraries/base/cbits\
+ libraries/base/tests\
+ utils/hp2ps\
+ utils/hpc\
+ utils/unlit\
+ --ignore-files\
+ libraries/base/GHC/IO/Encoding/CodePage/Table.hs\
+ libraries/base/Control/Concurrent/QSem.hs\
+ libraries/base/Control/Concurrent/QSemN.hs\
+ libraries/base/Control/Monad/ST/Imp.hs\
+ libraries/base/Control/Monad/ST/Lazy.hs\
+ libraries/base/Data/Char.hs\
+ libraries/base/Data/Eq.hs\
+ libraries/base/Data/IORef.hs\
+ libraries/base/Data/Int.hs\
+ libraries/base/Data/Ix.hs\
+ libraries/base/Data/Ratio.hs\
+ libraries/base/Data/STRef/Lazy.hs\
+ libraries/base/Data/STRef/Strict.hs\
+ libraries/base/Foreign.hs\
+ libraries/base/Foreign/C.hs\
+ libraries/base/Foreign/Concurrent.hs\
+ libraries/base/Foreign/ForeignPtr.hs\
+ libraries/base/Foreign/ForeignPtr/Imp.hs\
+ libraries/base/Foreign/ForeignPtr/Safe.hs\
+ libraries/base/Foreign/ForeignPtr/Unsafe.hs\
+ libraries/base/Foreign/Marshal.hs\
+ libraries/base/Foreign/Marshal/Alloc.hs\
+ libraries/base/Foreign/Marshal/Error.hs\
+ libraries/base/Foreign/Marshal/Safe.hs\
+ libraries/base/Foreign/Marshal/Unsafe.hs\
+ libraries/base/Foreign/Safe.hs\
+ libraries/base/Foreign/StablePtr.hs\
+ libraries/base/Foreign/Storable.hs\
+ libraries/base/GHC/IO/Encoding/Latin1.hs\
+ libraries/base/GHC/IO/Encoding/Types.hs\
+ libraries/base/GHC/IO/Handle/FD.hs\
+ libraries/base/GHC/IO/IOMode.hs\
+ libraries/base/System/Console/GetOpt.hs\
+ libraries/base/System/IO/Unsafe.hs\
+ libraries/base/System/Mem.hs\
+ libraries/base/Text/Show.hs\
+ libraries/base/include/HsBase.h\
+ libraries/base/include/HsEvent.h\
+ libraries/base/include/md5.h\
+ libraries/ghc-prim/GHC/Tuple.hs\
+ libraries/template-haskell/Language/Haskell/TH/Quote.hs\
+ rts/STM.h\
+ rts/Sparks.h\
+ rts/Threads.h\
+ rts/hooks/OnExit.c\
+ rts/sm/Evac.h\
+ rts/sm/MarkStack.h\
+ rts/sm/MarkWeak.h\
+ rts/sm/Scav.h\
+ rts/sm/Sweep.c\
+ rts/sm/Sweep.h\
+ rts/win32/veh_excn.h\
+ utils/genprimopcode/Parser.y\
+ utils/genprimopcode/Syntax.hs\
+ utils/lndir/lndir-Xos.h\
+ utils/lndir/lndir-Xosdefs.h\
+ )
diff --git a/testsuite/tests/linters/all.T b/testsuite/tests/linters/all.T
index ef3ea5441f..0eefb0f9a4 100644
--- a/testsuite/tests/linters/all.T
+++ b/testsuite/tests/linters/all.T
@@ -14,8 +14,27 @@ def has_ls_files() -> bool:
except subprocess.CalledProcessError:
return False
+test('makefiles', [ no_deps if has_ls_files() else skip
+ , extra_files(["regex-linters"]) ]
+ , makefile_test, ['makefiles'])
-test('notes', [no_deps if has_ls_files() else skip
- , req_hadrian_deps(["lint:notes-util"])
+test('changelogs', [ no_deps if has_ls_files() else skip
+ , extra_files(["regex-linters"]) ]
+ , makefile_test, ['changelogs'])
+
+test('cpp', [ no_deps if has_ls_files() else skip
+ , extra_files(["regex-linters"]) ]
+ , makefile_test, ['cpp'])
+
+test('version-number', [ no_deps if has_ls_files() else skip
+ , extra_files(["regex-linters"]) ]
+ , makefile_test, ['version-number'])
+
+test('notes', [ no_deps if has_ls_files() else skip
+ , req_hadrian_deps(["lint:notes"])
, normalise_fun(normalise_nos) ]
- , makefile_test, ['notes'])
+ , makefile_test, ['notes'])
+
+test('whitespace', [ no_deps if has_ls_files() else skip
+ , req_hadrian_deps(["lint:whitespace"]) ]
+ , makefile_test, ['whitespace'])
diff --git a/testsuite/tests/linters/changelogs.stdout b/testsuite/tests/linters/changelogs.stdout
new file mode 100644
index 0000000000..9547d7b24b
--- /dev/null
+++ b/testsuite/tests/linters/changelogs.stdout
@@ -0,0 +1 @@
+Changelogs look OK (no "TBA"s, or RELEASE=NO)
diff --git a/testsuite/tests/linters/notes.stdout b/testsuite/tests/linters/notes.stdout
index 8e4ad38ac8..57d2aa1367 100644
--- a/testsuite/tests/linters/notes.stdout
+++ b/testsuite/tests/linters/notes.stdout
@@ -22,11 +22,11 @@ ref compiler/GHC/Core/Unfold.hs:1242:50: Note [Unfold info lazy contexts]
ref compiler/GHC/Core/Unfold/Make.hs:157:34: Note [DFunUnfoldings]
ref compiler/GHC/Core/Unify.hs:544:16: Note [Unification result]
ref compiler/GHC/Core/Unify.hs:1390:9: Note [INLINE pragmas and (>>)]
-ref compiler/GHC/Core/Utils.hs:944:40: Note [ _ -> [con1]
-ref compiler/GHC/CoreToStg.hs:460:15: Note [Nullary unboxed tuple]
-ref compiler/GHC/Driver/Main.hs:1551:34: Note [simpleTidyPgm - mkBootModDetailsTc]
-ref compiler/GHC/Driver/Session.hs:1949:36: Note [GHC.Driver.Main . Safe Haskell Inference]
-ref compiler/GHC/Driver/Session.hs:3910:49: Note [Eta-reduction in -O0]
+ref compiler/GHC/Core/Utils.hs:947:40: Note [ _ -> [con1]
+ref compiler/GHC/CoreToStg.hs:462:15: Note [Nullary unboxed tuple]
+ref compiler/GHC/Driver/Main.hs:1566:34: Note [simpleTidyPgm - mkBootModDetailsTc]
+ref compiler/GHC/Driver/Session.hs:1947:36: Note [GHC.Driver.Main . Safe Haskell Inference]
+ref compiler/GHC/Driver/Session.hs:3916:49: Note [Eta-reduction in -O0]
ref compiler/GHC/Hs/Extension.hs:140:5: Note [Strict argument type constraints]
ref compiler/GHC/HsToCore/Binds.hs:313:33: Note [AbsBinds wrappers]
ref compiler/GHC/HsToCore/Binds.hs:849:46: Note [Free dictionaries]
@@ -39,8 +39,8 @@ ref compiler/GHC/HsToCore/Docs.hs:130:0: Note [1]
ref compiler/GHC/HsToCore/Pmc/Solver.hs:853:20: Note [COMPLETE sets on data families]
ref compiler/GHC/HsToCore/Types.hs:61:13: Note [Generating fresh names for FFI wrappers]
ref compiler/GHC/HsToCore/Utils.hs:939:62: Note [Don't CPR join points]
-ref compiler/GHC/Iface/Syntax.hs:705:0: Note [Minimal complete definition]
-ref compiler/GHC/Iface/Syntax.hs:765:44: Note [Minimal complete definition]
+ref compiler/GHC/Iface/Syntax.hs:708:0: Note [Minimal complete definition]
+ref compiler/GHC/Iface/Syntax.hs:768:44: Note [Minimal complete definition]
ref compiler/GHC/Parser/Lexer.x:185:7: Note [Lexing NumericUnderscores extension]
ref compiler/GHC/Parser/Lexer.x:502:3: Note [Lexing NumericUnderscores extension]
ref compiler/GHC/Rename/Expr.hs:2013:9: Note [ApplicativeDo and strict patterns]
@@ -52,10 +52,10 @@ ref compiler/GHC/Rename/Pat.hs:888:29: Note [Disambiguating record fields
ref compiler/GHC/Rename/Splice.hs:450:27: Note [Splices]
ref compiler/GHC/Runtime/Eval.hs:996:2: Note [Querying instances for a type]
ref compiler/GHC/Runtime/Interpreter.hs:198:30: Note [uninterruptibleMask_]
-ref compiler/GHC/StgToCmm.hs:107:18: Note [codegen-split-init]
-ref compiler/GHC/StgToCmm.hs:110:18: Note [pipeline-split-init]
-ref compiler/GHC/StgToCmm/Expr.hs:491:4: Note [case on bool]
-ref compiler/GHC/StgToCmm/Expr.hs:751:3: Note [alg-alt heap check]
+ref compiler/GHC/StgToCmm.hs:108:18: Note [codegen-split-init]
+ref compiler/GHC/StgToCmm.hs:111:18: Note [pipeline-split-init]
+ref compiler/GHC/StgToCmm/Expr.hs:585:4: Note [case on bool]
+ref compiler/GHC/StgToCmm/Expr.hs:848:3: Note [alg-alt heap check]
ref compiler/GHC/Tc/Errors.hs:180:13: Note [Fail fast on kind errors]
ref compiler/GHC/Tc/Errors.hs:2016:0: Note [Highlighting ambiguous type variables]
ref compiler/GHC/Tc/Errors/Ppr.hs:1760:11: Note [Highlighting ambiguous type variables]
@@ -82,10 +82,10 @@ ref compiler/GHC/Tc/Gen/Pat.hs:1376:16: Note [Pattern coercions]
ref compiler/GHC/Tc/Gen/Sig.hs:78:10: Note [Overview of type signatures]
ref compiler/GHC/Tc/Instance/Family.hs:515:35: Note [Constrained family instances]
ref compiler/GHC/Tc/Module.hs:698:15: Note [Extra dependencies from .hs-boot files]
-ref compiler/GHC/Tc/Module.hs:1901:19: Note [Root-main id]
-ref compiler/GHC/Tc/Module.hs:1971:6: Note [Root-main id]
+ref compiler/GHC/Tc/Module.hs:1904:19: Note [Root-main id]
+ref compiler/GHC/Tc/Module.hs:1974:6: Note [Root-main id]
ref compiler/GHC/Tc/Solver.hs:2541:36: Note [Kind generalisation and SigTvs]
-ref compiler/GHC/Tc/Solver/Canonical.hs:1228:33: Note [Canonical LHS]
+ref compiler/GHC/Tc/Solver/Canonical.hs:1229:33: Note [Canonical LHS]
ref compiler/GHC/Tc/Solver/Interact.hs:1638:9: Note [No touchables as FunEq RHS]
ref compiler/GHC/Tc/Solver/Interact.hs:2292:12: Note [The equality class story]
ref compiler/GHC/Tc/Solver/Rewrite.hs:1032:7: Note [Stability of rewriting]
@@ -96,9 +96,9 @@ ref compiler/GHC/Tc/TyCl.hs:4366:16: Note [rejigCon and c.f. Note [Check
ref compiler/GHC/Tc/TyCl/Instance.hs:947:26: Note [Generalising in tcFamTyPatsGuts]
ref compiler/GHC/Tc/Types.hs:647:17: Note [Generating fresh names for FFI wrappers]
ref compiler/GHC/Tc/Types.hs:696:33: Note [Extra dependencies from .hs-boot files]
-ref compiler/GHC/Tc/Types.hs:1145:28: Note [Don't promote data constructors with non-equality contexts]
-ref compiler/GHC/Tc/Types.hs:1221:36: Note [Bindings with closed types]
-ref compiler/GHC/Tc/Types.hs:1457:47: Note [Care with plugin imports]
+ref compiler/GHC/Tc/Types.hs:1154:28: Note [Don't promote data constructors with non-equality contexts]
+ref compiler/GHC/Tc/Types.hs:1230:36: Note [Bindings with closed types]
+ref compiler/GHC/Tc/Types.hs:1466:47: Note [Care with plugin imports]
ref compiler/GHC/Tc/Types/Constraint.hs:238:34: Note [NonCanonical Semantics]
ref compiler/GHC/Tc/Utils/Concrete.hs:246:2: Note [Concrete and Concrete#]
ref compiler/GHC/Tc/Utils/Env.hs:556:7: Note [Bindings with closed types]
@@ -109,8 +109,8 @@ ref compiler/GHC/Tc/Utils/TcMType.hs:793:7: Note [Kind checking for GADTs
ref compiler/GHC/Tc/Utils/TcType.hs:529:7: Note [TyVars and TcTyVars]
ref compiler/GHC/ThToHs.hs:1738:11: Note [Adding parens for splices]
ref compiler/GHC/ThToHs.hs:1749:3: Note [Adding parens for splices]
-ref compiler/GHC/Types/Basic.hs:586:17: Note [Safe Haskell isSafeOverlap]
-ref compiler/GHC/Types/Basic.hs:1326:7: Note [Activation competition]
+ref compiler/GHC/Types/Basic.hs:619:17: Note [Safe Haskell isSafeOverlap]
+ref compiler/GHC/Types/Basic.hs:1359:7: Note [Activation competition]
ref compiler/GHC/Types/Demand.hs:307:25: Note [Preserving Boxity of results is rarely a win]
ref compiler/GHC/Types/Demand.hs:1100:4: Note [Use one-shot information]
ref compiler/GHC/Types/Error.hs:358:3: Note [Suppressing Messages]
@@ -124,14 +124,16 @@ ref compiler/Language/Haskell/Syntax/Expr.hs:1561:32: Note [Quasi-quote o
ref compiler/Language/Haskell/Syntax/Pat.hs:336:12: Note [Disambiguating record fields]
ref configure.ac:212:10: Note [Linking ghc-bin against threaded stage0 RTS]
ref docs/core-spec/core-spec.mng:177:6: Note [TyBinders]
-ref ghc/GHCi/UI.hs:3630:25: Note [ModBreaks.decls]
+ref ghc/GHCi/UI.hs:3646:25: Note [ModBreaks.decls]
ref ghc/ghc.mk:62:6: Note [Linking ghc-bin against threaded stage0 RTS]
ref hadrian/src/Expression.hs:130:30: Note [Linking ghc-bin against threaded stage0 RTS]
ref libraries/base/GHC/ST.hs:139:7: Note [Definition of runRW#]
+ref linters/lint-notes/Notes.hs:32:29: Note [" <> T.unpack x <> "]
+ref linters/lint-notes/Notes.hs:69:22: Note [...]
ref testsuite/config/ghc:212:10: Note [WayFlags]
ref testsuite/driver/testlib.py:152:10: Note [Why is there no stage1 setup function?]
ref testsuite/driver/testlib.py:156:2: Note [Why is there no stage1 setup function?]
-ref testsuite/mk/boilerplate.mk:259:2: Note [WayFlags]
+ref testsuite/mk/boilerplate.mk:263:2: Note [WayFlags]
ref testsuite/tests/indexed-types/should_fail/ExtraTcsUntch.hs:30:27: Note [Extra TcS Untouchables]
ref testsuite/tests/perf/join_points/join005.hs:19:63: Note [Don't CPR join points]
ref testsuite/tests/perf/should_run/all.T:3:6: Note [Solving from instances when interacting Dicts]
@@ -149,7 +151,5 @@ ref testsuite/tests/typecheck/should_compile/tc228.hs:9:7: Note [Inferenc
ref testsuite/tests/typecheck/should_compile/tc231.hs:12:16: Note [Important subtlety in oclose]
ref testsuite/tests/typecheck/should_fail/UnliftedNewtypesMultiFieldGadt.hs:11:28: Note [Kind-checking the field type]
ref testsuite/tests/typecheck/should_fail/tcfail093.hs:13:7: Note [Important subtlety in oclose]
-ref utils/notes-util/Notes.hs:33:29: Note [" <> T.unpack x <> "]
-ref utils/notes-util/Notes.hs:70:22: Note [...]
-ref validate:413:14: Note [Why is there no stage1 setup function?]
+ref validate:412:14: Note [Why is there no stage1 setup function?]
diff --git a/testsuite/tests/linters/regex-linters/check-changelogs.sh b/testsuite/tests/linters/regex-linters/check-changelogs.sh
new file mode 100755
index 0000000000..10fca9d5c0
--- /dev/null
+++ b/testsuite/tests/linters/regex-linters/check-changelogs.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+set -e
+
+COLOR_RED="\e[31m"
+COLOR_GREEN="\e[32m"
+COLOR_NONE="\e[0m"
+
+if grep -E -q 'RELEASE=YES' ${1}/configure.ac && grep TBA ${1}/libraries/*/changelog.md
+then
+ echo -e "${COLOR_RED}Error: Found \"TBA\"s in changelogs.${COLOR_NONE}"
+ exit 1
+else
+ echo -e "${COLOR_GREEN}Changelogs look OK (no \"TBA\"s, or RELEASE=NO)${COLOR_NONE}"
+ exit 0
+fi
diff --git a/testsuite/tests/linters/regex-linters/check-cpp.py b/testsuite/tests/linters/regex-linters/check-cpp.py
new file mode 100644
index 0000000000..4cc2257984
--- /dev/null
+++ b/testsuite/tests/linters/regex-linters/check-cpp.py
@@ -0,0 +1,44 @@
+#!/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 pathlib import Path
+from linter import run_linters, RegexpLinter
+
+linters = [
+ RegexpLinter(r'WARN\s+\(',
+ message='CPP macros should not have a space between the macro name and their argument list'),
+ 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`'),
+]
+
+for l in linters:
+ # Need do document rules!
+ l.add_path_filter(lambda path: path != Path('docs', 'coding-style.html'))
+ l.add_path_filter(lambda path: path != Path('docs', 'users_guide', 'utils.rst'))
+ # Don't lint vendored code
+ l.add_path_filter(lambda path: not path.name == 'config.guess')
+ # Don't lint files from external xxhash projects
+ l.add_path_filter(lambda path: path != Path('rts', 'xxhash.h')),
+ # Don't lint font files
+ l.add_path_filter(lambda path: not path.parent == Path('docs','users_guide',
+ 'rtd-theme', 'static', 'fonts'))
+ # Don't lint image files
+ l.add_path_filter(lambda path: not path.parent == Path('docs','users_guide',
+ 'images'))
+ # Don't lint core spec
+ l.add_path_filter(lambda path: not path.name == 'core-spec.pdf')
+ # Don't lint the linter itself
+ l.add_path_filter(lambda path: not path.name == 'check-cpp.py')
+
+if __name__ == '__main__':
+ run_linters(linters)
diff --git a/testsuite/tests/linters/regex-linters/check-makefiles.py b/testsuite/tests/linters/regex-linters/check-makefiles.py
new file mode 100644
index 0000000000..5a8286c6a7
--- /dev/null
+++ b/testsuite/tests/linters/regex-linters/check-makefiles.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+
+"""
+Linters for testsuite makefiles
+"""
+
+from linter import run_linters, RegexpLinter
+
+"""
+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.
+"""
+interactive_linter = \
+ RegexpLinter(r'--interactive',
+ message = "Warning: Use `$(TEST_HC_OPTS_INTERACTIVE)` instead of `--interactive -ignore-dot-ghci -v0`."
+ ).add_path_filter(lambda path: path.name == 'Makefile')
+
+test_hc_quotes_linter = \
+ RegexpLinter('\t\\$\\(TEST_HC\\)',
+ message = "Warning: $(TEST_HC) should be quoted in Makefiles.",
+ ).add_path_filter(lambda path: path.name == 'Makefile')
+
+ghc_pkg_quotes_linter = \
+ RegexpLinter('\t\\$\\(GHC_PKG\\)',
+ message = "Warning: $(GHC_PKG) should be quoted in Makefiles.",
+ ).add_path_filter(lambda path: path.name == 'Makefile')
+
+haddock_quotes_linter = \
+ RegexpLinter('\t\\$\\(HADDOCK\\)',
+ message = "Warning: $(HADDOCK) should be quoted in Makefiles.",
+ ).add_path_filter(lambda path: path.name == 'Makefile')
+
+linters = [
+ interactive_linter,
+ test_hc_quotes_linter,
+ ghc_pkg_quotes_linter,
+ haddock_quotes_linter
+]
+
+if __name__ == '__main__':
+ run_linters(linters,
+ subdir='testsuite')
diff --git a/testsuite/tests/linters/regex-linters/check-version-number.sh b/testsuite/tests/linters/regex-linters/check-version-number.sh
new file mode 100755
index 0000000000..2ce68627f4
--- /dev/null
+++ b/testsuite/tests/linters/regex-linters/check-version-number.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -e
+
+grep -E -q 'RELEASE=NO' ${1}/configure.ac ||
+ grep -E -q '\[[0-9]+\.[0-9]+\.[0-9]+\]' ${1}/configure.ac ||
+ ( echo "error: configure.ac: GHC version number must have three components when RELEASE=YES."; exit 1 )
diff --git a/testsuite/tests/linters/regex-linters/linter.py b/testsuite/tests/linters/regex-linters/linter.py
new file mode 100644
index 0000000000..2246dcce30
--- /dev/null
+++ b/testsuite/tests/linters/regex-linters/linter.py
@@ -0,0 +1,140 @@
+"""
+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')
+
+def get_tracked_files(subdir: str = '.'):
+ """ Get the files tracked by git in the given subdirectory. """
+ cmd = ['git', 'ls-tree', '--name-only', '-r', 'HEAD', 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 the given files')
+ subparser.add_argument('file', nargs='+', help='File to lint')
+ subparser.set_defaults(get_linted_files=lambda args: args.file)
+
+ subparser = subparsers.add_parser('tracked', help="Lint files tracked by Git")
+ subparser.set_defaults(get_linted_files=lambda args:
+ get_tracked_files(subdir))
+
+ args = parser.parse_args()
+
+ linted_files = args.get_linted_files(args)
+ for path in linted_files:
+ if path.startswith('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)
diff --git a/testsuite/tests/linters/whitespace.stdout b/testsuite/tests/linters/whitespace.stdout
new file mode 100644
index 0000000000..b773a1831d
--- /dev/null
+++ b/testsuite/tests/linters/whitespace.stdout
@@ -0,0 +1 @@
+whitespace validation passed!