mirror of
https://github.com/martinus/unordered_dense.git
synced 2026-01-18 17:21:27 +01:00
refactor: clean up linter scripts
This commit is contained in:
8
.github/copilot-instructions.md
vendored
8
.github/copilot-instructions.md
vendored
@@ -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.
|
||||
|
||||
@@ -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
27
scripts/lint/all.py
Executable 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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user