refactor: clean up linter scripts

This commit is contained in:
Martin Leitner-Ankerl
2025-10-27 17:25:22 +01:00
parent 3b454f0ebe
commit e6dbba2b67
7 changed files with 119 additions and 155 deletions

View File

@@ -99,7 +99,7 @@ All lint scripts are in `scripts/lint/`. They MUST pass before submitting change
```bash
# Run ALL linters (recommended)
./scripts/lint/lint-all.py
./scripts/lint/all.py
# Individual linters
./scripts/lint/lint-version.py # Check version consistency across files
@@ -149,7 +149,7 @@ All lint scripts are in `scripts/lint/`. They MUST pass before submitting change
### Common CI Failures & Fixes
1. **Linting failures**: Run `./scripts/lint/lint-all.py` locally first
1. **Linting failures**: Run `./scripts/lint/all.py` locally first
2. **32-bit failures**: Don't use `size_t` in hashes without consideration
3. **Sanitizer failures**: Check for undefined behavior, use-after-free, data races
4. **Windows/MSVC**: Check for MSVC-specific warnings (see `test/meson.build`)
@@ -216,7 +216,7 @@ unordered_dense/
5. **Lint**: Verify all linters pass
```bash
./scripts/lint/lint-all.py
./scripts/lint/all.py
```
6. **Multi-config** (optional): Test across configurations
@@ -300,4 +300,4 @@ unordered_dense/
- You need information not covered here
- The repository structure has changed significantly
When in doubt, run `./scripts/lint/lint-all.py` and `meson test -C builddir/<config> -v` to verify your changes.
When in doubt, run `./scripts/lint/all.py` and `meson test -C builddir/<config> -v` to verify your changes.

View File

@@ -54,7 +54,7 @@ def run(cmd):
if result.returncode != 0:
exit(result.returncode)
run('scripts/lint/lint-all.py')
run('scripts/lint/all.py')
for cmd_dir in cmd_and_dir:
workdir = cmd_dir[-1]

27
scripts/lint/all.py Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python3
from pathlib import Path
from subprocess import run
from time import perf_counter
def main():
start = perf_counter()
linters_dir = Path(__file__).parent
linters = sorted(
p for p in linters_dir.iterdir() if p.is_file() and p.name.startswith("lint-")
)
rc = 0
for lint in linters:
res = run([str(lint)])
if res.returncode:
print(f"^---- failure(s) from {lint.name}\n")
rc |= res.returncode
print(f"{len(linters)} linters in {perf_counter() - start:0.2f}s")
raise SystemExit(rc)
if __name__ == "__main__":
main()

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env python3
from glob import glob
from pathlib import Path
from subprocess import run
from os import path
from time import time
time_start = time()
exit_code = 0
num_linters = 0
mod_path = Path(__file__).parent
for lint in glob(f"{mod_path}/lint-*"):
lint = path.abspath(lint)
if lint == path.abspath(__file__):
continue
num_linters += 1
result = run([lint])
if result.returncode == 0:
continue
print(f"^---- failure from {lint.split('/')[-1]}")
exit_code |= result.returncode
time_end = time()
print(f"{num_linters} linters in {time_end - time_start:0.2}s")
exit(exit_code)

View File

@@ -1,50 +1,38 @@
#!/usr/bin/env python3
from glob import glob
from pathlib import Path
from subprocess import run
from os import path
import subprocess
import sys
from time import time
import re
import shutil
from subprocess import run
import sys
root_path = path.abspath(Path(__file__).parent.parent.parent)
ROOT = Path(__file__).resolve().parents[2]
PATTERNS = ["include/**/*.h", "test/**/*.h", "test/**/*.cpp"]
EXCLUDE_RE = re.compile(r"nanobench\.h|FuzzedDataProvider\.h|/third-party/")
globs = [
f"{root_path}/include/**/*.h",
f"{root_path}/test/**/*.h",
f"{root_path}/test/**/*.cpp",
]
exclusions = [
"nanobench\\.h",
"FuzzedDataProvider\\.h",
'/third-party/']
files = []
for g in globs:
r = glob(g, recursive=True)
files.extend(r)
def collect_files(root: Path):
return [
f
for p in PATTERNS
for f in root.glob(p)
if f.is_file() and not EXCLUDE_RE.search(str(f))
]
# filter out exclusions
for exclusion in exclusions:
l = filter(lambda file: re.search(exclusion, file) == None, files)
files = list(l)
if len(files) == 0:
print("could not find any files!")
sys.exit(1)
def main():
files = collect_files(ROOT)
if not files:
print("could not find any files!")
raise SystemExit(1)
command = ['clang-format', '--dry-run', '-Werror'] + files
p = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=None,
stdin=subprocess.PIPE,
universal_newlines=True)
if not (clang_format := shutil.which("clang-format")):
print("clang-format not found in PATH")
raise SystemExit(2)
stdout, stderr = p.communicate()
ec = run([clang_format, "--dry-run", "-Werror"] + files).returncode
print(f"clang-format checked {len(files)} files")
SystemExit(ec)
print(f"clang-format checked {len(files)} files")
if p.returncode != 0:
sys.exit(p.returncode)
if __name__ == "__main__":
main()

View File

@@ -1,44 +1,37 @@
#!/usr/bin/env python3
"""Run clang-tidy only on unordered_dense.h"""
import argparse
import subprocess
import shutil
from subprocess import run
import sys
from pathlib import Path
def main():
parser = argparse.ArgumentParser(
description="Run clang-tidy on unordered_dense.h (via include_only.cpp)"
p = argparse.ArgumentParser(
description=(
"Run clang-tidy on unordered_dense.h (via test/unit/include_only.cpp)"
)
)
parser.add_argument(
"--std",
default="c++17",
help="C++ standard to use (default: c++17)",
)
args = parser.parse_args()
p.add_argument("--std", default="c++17", help="C++ standard (default: c++17)")
args = p.parse_args()
# Define paths
source_file = Path("test/unit/include_only.cpp")
cmd = [
"clang-tidy",
str(source_file),
"--header-filter=.*unordered_dense\\.h",
"--warnings-as-errors=*",
"--",
f"-std={args.std}",
"-I",
"include",
]
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e:
sys.exit(e.returncode)
except FileNotFoundError:
if not (clang_tidy := shutil.which("clang-tidy")):
print("Error: clang-tidy not found in PATH", file=sys.stderr)
sys.exit(1)
raise SystemExit(1)
# Exit with clang-tidy's exit code.
ec = run(
[
clang_tidy,
"test/unit/include_only.cpp",
"--header-filter=.*unordered_dense\\.h",
"--warnings-as-errors=*",
"--",
f"-std={args.std}",
"-I",
"include",
]
).returncode
raise SystemExit(ec)
if __name__ == "__main__":

View File

@@ -1,59 +1,45 @@
#!/usr/bin/env python3
import os
import pathlib
from pathlib import Path
import re
root = os.path.abspath(pathlib.Path(__file__).parent.parent.parent)
# filename, pattern, number of occurrences
file_pattern_count = [
(f"{root}/CMakeLists.txt", r"^\s+VERSION (\d+)\.(\d+)\.(\d+)\n", 1),
(f"{root}/include/ankerl/stl.h", r"Version (\d+)\.(\d+)\.(\d+)\n", 1),
(f"{root}/include/ankerl/unordered_dense.h", r"Version (\d+)\.(\d+)\.(\d+)\n", 1),
(f"{root}/meson.build", r"version: '(\d+)\.(\d+)\.(\d+)'", 1),
(f"{root}/test/unit/namespace.cpp", r"unordered_dense::v(\d+)_(\d+)_(\d+)", 1),
# fmt: off
ROOT = Path(__file__).resolve().parents[2]
HEADER = ROOT / "include" / "ankerl" / "unordered_dense.h"
CHECKS = [
(HEADER, r"Version (\d+)\.(\d+)\.(\d+)", 1),
(ROOT / "CMakeLists.txt", r"^\s+VERSION (\d+)\.(\d+)\.(\d+)", 1),
(ROOT / "include" / "ankerl" / "stl.h", r"Version (\d+)\.(\d+)\.(\d+)", 1),
(ROOT / "meson.build", r"version:\s*'?(\d+)\.(\d+)\.(\d+)'?", 1),
(ROOT / "test" / "unit" / "namespace.cpp", r"unordered_dense::v(\d+)_(\d+)_(\d+)", 1),
]
# fmt: on
# let's parse the reference from svector.h
major = "??"
minor = "??"
patch = "??"
with open(f"{root}/include/ankerl/unordered_dense.h", "r") as f:
for line in f:
r = re.search(r"#define ANKERL_UNORDERED_DENSE_VERSION_([A-Z]+) (\d+)", line)
if not r:
continue
if "MAJOR" == r.group(1):
major = r.group(2)
elif "MINOR" == r.group(1):
minor = r.group(2)
elif "PATCH" == r.group(1):
patch = r.group(2)
else:
"match but with something else!"
exit(1)
def read_version_from_header(p: Path) -> str:
m = re.findall(
r"#define\s+ANKERL_UNORDERED_DENSE_VERSION_(MAJOR|MINOR|PATCH)\s+(\d+)",
p.read_text(),
)
d = dict(m)
return f"{d['MAJOR']}.{d['MINOR']}.{d['PATCH']}"
is_ok = True
for filename, pattern, count in file_pattern_count:
num_found = 0
with open(filename, "r") as f:
for line in f:
r = re.search(pattern, line)
if r:
num_found += 1
if major != r.group(1) or minor != r.group(2) or patch != r.group(3):
is_ok = False
print(
f"ERROR in {filename}: got '{line.strip()}' but version should be '{major}.{minor}.{patch}'"
)
if num_found != count:
is_ok = False
print(
f"ERROR in {filename}: expected {count} occurrences but found it {num_found} times"
def main():
ref = read_version_from_header(HEADER)
errs = []
for path, pattern, count in CHECKS:
matches = list(re.finditer(pattern, path.read_text(), re.M))
if (n := len(matches)) != count:
errs.append(f"ERROR: {path}: expected {count} matches, found {n}")
errs.extend(
f"ERROR: {path}: found version {found}, expected {ref}"
for m in matches
if (found := ".".join(m.groups())) != ref
)
if not is_ok:
exit(1)
print("\n".join(errs))
raise SystemExit(1 if errs else 0)
if __name__ == "__main__":
main()