From a59a66a8163cbbfa49a14f3c91af322f906084ea Mon Sep 17 00:00:00 2001
From: Ben Gamari <ben@smart-cactus.org>
Date: Tue, 5 Apr 2022 23:09:47 -0400
Subject: [PATCH] testsuite: Lint RTS #includes

Verifies two important properties of #includes in the RTS:

 * That system headers don't appear inside of a `<BeginPrivate.h>` block
   as this can hide system library symbols, resulting in very
   hard-to-diagnose linker errors

 * That no headers precede `Rts.h`, ensuring that __USE_MINGW_ANSI_STDIO
   is set correctly before system headers are included.
---
 testsuite/tests/linters/Makefile              |  3 +
 testsuite/tests/linters/all.T                 |  6 +-
 .../regex-linters/check-rts-includes.py       | 91 +++++++++++++++++++
 3 files changed, 99 insertions(+), 1 deletion(-)
 create mode 100755 testsuite/tests/linters/regex-linters/check-rts-includes.py

diff --git a/testsuite/tests/linters/Makefile b/testsuite/tests/linters/Makefile
index 54ef4db1324d..2b4c2ad2c37c 100644
--- a/testsuite/tests/linters/Makefile
+++ b/testsuite/tests/linters/Makefile
@@ -20,6 +20,9 @@ version-number:
 cpp:
 	(cd $(TOP)/tests/linters/ && python3 regex-linters/check-cpp.py tracked)
 
+rts-includes:
+	(cd $(TOP)/tests/linters/ && python3 regex-linters/check-rts-includes.py tracked)
+
 changelogs:
 	regex-linters/check-changelogs.sh $(TOP)/..
 
diff --git a/testsuite/tests/linters/all.T b/testsuite/tests/linters/all.T
index 16700869a470..0e06df6d501a 100644
--- a/testsuite/tests/linters/all.T
+++ b/testsuite/tests/linters/all.T
@@ -23,7 +23,11 @@ test('changelogs', [ no_deps if has_ls_files() else skip
 
 test('cpp', [ no_deps if has_ls_files() else skip
             , extra_files(["regex-linters"]) ]
-            , makefile_test, ['cpp'])
+          , makefile_test, ['cpp'])
+
+test('rts-includes', [ no_deps if has_ls_files() else skip
+                     , extra_files(["regex-linters"]) ]
+                   , makefile_test, ['rts-includes'])
 
 test('version-number', [ no_deps if has_ls_files() else skip
                        , extra_files(["regex-linters"]) ]
diff --git a/testsuite/tests/linters/regex-linters/check-rts-includes.py b/testsuite/tests/linters/regex-linters/check-rts-includes.py
new file mode 100755
index 000000000000..14f22995b677
--- /dev/null
+++ b/testsuite/tests/linters/regex-linters/check-rts-includes.py
@@ -0,0 +1,91 @@
+#!/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, Linter, Warning
+
+from typing import List, Tuple
+import re
+
+INCLUDE_RE = re.compile('# *include ([<"][^">]+[>"])')
+
+def get_includes(file: Path) -> List[Tuple[int, str]]:
+    txt = file.read_text()
+    return [ (line_no+1, m.group(1) )
+             for (line_no, line) in enumerate(txt.split('\n'))
+             for m in [INCLUDE_RE.match(line)]
+             if m is not None
+             if m.group(1) != "rts/PosixSource.h"]
+
+def in_rts_dir(path: Path) -> bool:
+    return len(path.parts) > 0 and path.parts[0] == 'rts'
+
+class RtsHIncludeOrderLinter(Linter):
+    """
+    Verify that "PosixSource.h" is always the first #include in source files to
+    ensure __USE_MINGW_ANSI_STDIO is defined before system headers are
+    #include'd.
+    """
+    def __init__(self):
+        Linter.__init__(self)
+        self.add_path_filter(in_rts_dir)
+        self.add_path_filter(lambda path: path.suffix == '.c')
+
+    def lint(self, path: Path):
+        # We do allow a few small headers to precede Rts.h
+        ALLOWED_HEADERS = {
+            '"ghcconfig.h"',
+            '"ghcplatform.h"',
+        }
+
+        includes = get_includes(path)
+        headers = [x[1] for x in includes]
+        lines = path.read_text().split('\n')
+
+        if '"PosixSource.h"' in headers:
+            for line_no, header in includes:
+                if header == '"PosixSource.h"':
+                    break
+                elif header in ALLOWED_HEADERS:
+                    continue
+
+                self.add_warning(Warning(
+                    path=path,
+                    line_no=line_no,
+                    line_content=lines[line_no-1],
+                    message="PosixSource.h must be first header included in each file"))
+
+class PrivateIncludeLinter(Linter):
+    """
+    Verify that system headers are not #include'd in <BeginPrivate.h> blocks as this
+    can result in very hard-to-diagnose linking errors due to hidden library functions.
+    """
+    def __init__(self):
+        Linter.__init__(self)
+        self.add_path_filter(in_rts_dir)
+        self.add_path_filter(lambda path: path.suffix == '.h')
+
+    def lint(self, path: Path):
+        private = False
+        lines = path.read_text().split('\n')
+        for line_no, include in get_includes(path):
+            if include == '"BeginPrivate.h"':
+                private = True
+            elif include == '"EndPrivate.h"':
+                private = False
+            elif private:
+                self.add_warning(Warning(
+                    path=path,
+                    line_no=line_no,
+                    line_content=lines[line_no-1],
+                    message='System header %s found inside of <BeginPrivate.h> block' % include))
+
+linters = [
+    RtsHIncludeOrderLinter(),
+    PrivateIncludeLinter(),
+]
+
+if __name__ == '__main__':
+    run_linters(linters)
-- 
GitLab