Add CI test that `#[cfg]` tags are from a defined set 2023-12-check-cfg-tags
authorMatt Corallo <git@bluematt.me>
Fri, 8 Dec 2023 23:05:37 +0000 (23:05 +0000)
committerMatt Corallo <git@bluematt.me>
Fri, 8 Dec 2023 23:07:14 +0000 (23:07 +0000)
Rust is fairly relaxed in checking the validity of arguments
passed to #[cfg]. While it should probably be more strict when
checking features, it cannot be strict when checking loose cfg
tags, because those can be anything and are simply passed to rustc
via unconstrained arguments.

Thus, we do it for rustc manually, but scanning all our source and
checking that all our cfg tags match a known cfg tag.

Fixes #2184

.github/workflows/build.yml
ci/check-cfg-flags.py [new file with mode: 0755]

index 00ef76f787e188e0e23fc94b2244bb1cbca8673f..91f61810e5f9dc3059ec5cfc94e41b9b4c71f5cc 100644 (file)
@@ -44,6 +44,8 @@ jobs:
         run: |
           rustup target add thumbv7m-none-eabi
           sudo apt-get -y install gcc-arm-none-eabi
+      - name: Check for unknown cfg tags
+        run: ci/check-cfg-flags.py
       - name: shellcheck the CI script
         if: "matrix.platform == 'ubuntu-latest'"
         run: |
diff --git a/ci/check-cfg-flags.py b/ci/check-cfg-flags.py
new file mode 100755 (executable)
index 0000000..85cbde8
--- /dev/null
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+# Rust is fairly relaxed in checking the validity of arguments passed to #[cfg].
+# While it should probably be more strict when checking features, it cannot be
+# strict when checking loose cfg tags, because those can be anything and are
+# simply passed to rustc via unconstrained arguments.
+#
+# Thus, we do it for rustc manually, but scanning all our source and checking
+# that all our cfg tags match a known cfg tag.
+import sys, glob, re
+
+def check_feature(feature):
+    if feature == "std":
+        pass
+    elif feature == "no-std":
+        pass
+    elif feature == "hashbrown":
+        pass
+    elif feature == "backtrace":
+        pass
+    elif feature == "grind_signatures":
+        pass
+    elif feature == "unsafe_revoked_tx_signing":
+        pass
+    elif feature == "futures":
+        pass
+    elif feature == "tokio":
+        pass
+    elif feature == "rest-client":
+        pass
+    elif feature == "rpc-client":
+        pass
+    elif feature == "serde":
+        pass
+    elif feature == "esplora-blocking":
+        pass
+    elif feature == "esplora-async":
+        pass
+    elif feature == "async-interface":
+        pass
+    elif feature == "electrum":
+        pass
+    elif feature == "_test_utils":
+        pass
+    elif feature == "_test_vectors":
+        pass
+    elif feature == "afl":
+        pass
+    elif feature == "honggfuzz":
+        pass
+    elif feature == "libfuzzer_fuzz":
+        pass
+    elif feature == "stdin_fuzz":
+        pass
+    elif feature == "max_level_off":
+        pass
+    elif feature == "max_level_error":
+        pass
+    elif feature == "max_level_warn":
+        pass
+    elif feature == "max_level_info":
+        pass
+    elif feature == "max_level_debug":
+        pass
+    elif feature == "max_level_trace":
+        pass
+    else:
+        print("Bad feature: " + feature)
+        assert False
+
+def check_target_os(os):
+    if os == "windows":
+        pass
+    else:
+        assert False
+
+def check_cfg_tag(cfg):
+    if cfg == "fuzzing":
+        pass
+    elif cfg == "test":
+        pass
+    elif cfg == "debug_assertions":
+        pass
+    elif cfg == "c_bindings":
+        pass
+    elif cfg == "ldk_bench":
+        pass
+    elif cfg == "taproot":
+        pass
+    elif cfg == "require_route_graph_test":
+        pass
+    else:
+        print("Bad cfg tag: " + cfg)
+        assert False
+
+def check_cfg_args(cfg):
+    if cfg.startswith("all(") or cfg.startswith("any(") or cfg.startswith("not("):
+        brackets = 1
+        pos = 4
+        while pos < len(cfg):
+            if cfg[pos] == "(":
+                brackets += 1
+            elif cfg[pos] == ")":
+                brackets -= 1
+                if brackets == 0:
+                    check_cfg_args(cfg[4:pos])
+                    if pos + 1 != len(cfg):
+                        assert cfg[pos + 1] == ","
+                        check_cfg_args(cfg[pos + 2:].strip())
+                    return
+            pos += 1
+        assert False
+        assert(cfg.endswith(")"))
+        check_cfg_args(cfg[4:len(cfg)-1])
+    else:
+        parts = [part.strip() for part in cfg.split(",", 1)]
+        if len(parts) > 1:
+            for part in parts:
+                check_cfg_args(part)
+        elif cfg.startswith("feature") or cfg.startswith("target_os") or cfg.startswith("target_pointer_width"):
+            arg = cfg
+            if cfg.startswith("feature"):
+                arg = arg[7:].strip()
+            elif cfg.startswith("target_os"):
+                arg = arg[9:].strip()
+            else:
+                arg = arg[20:].strip()
+            assert arg.startswith("=")
+            arg = arg[1:].strip()
+            assert arg.startswith("\"")
+            assert arg.endswith("\"")
+            arg = arg[1:len(arg)-1]
+            assert not "\"" in arg
+            if cfg.startswith("feature"):
+                check_feature(arg)
+            elif cfg.startswith("target_os"):
+                check_target_os(arg)
+            else:
+                assert arg == "32" or arg == "64"
+        else:
+            check_cfg_tag(cfg.strip())
+
+cfg_regex = re.compile("#\[cfg\((.*)\)\]")
+for path in glob.glob(sys.path[0] + "/../**/*.rs", recursive = True):
+    with open(path, "r") as file:
+        while True:
+            line = file.readline()
+            if not line:
+                break
+            if "#[cfg(" in line:
+                if not line.strip().startswith("//"):
+                    cfg_part = cfg_regex.match(line.strip()).group(1)
+                    check_cfg_args(cfg_part)