Added clang-tidy ban of qMin, qMax, qAbs and qBound.

This commit is contained in:
23rd
2026-09-14 16:22:17 +03:00
parent 346d0f3a85
commit 52b50714c2
3 changed files with 162 additions and 0 deletions

13
tools/clangtidy/README.md Normal file
View File

@@ -0,0 +1,13 @@
# clang-tidy for tdesktop
```bash
python3 tools/clangtidy/compile_db.py # after each configure
git diff -U0 HEAD | clang-tidy-diff.py -p1 -path out/clang-tidy -j 10 -quiet
run-clang-tidy -p out/clang-tidy -j 10 -quiet <files>
```
The second line is the gate: it reports only the lines a commit touches. Both
runners ship with LLVM, under `share/clang` next to the binary.
Needs clang-tidy 20 or newer -- an older one ignores `CustomFunctions`, which
the bans in `.clang-tidy` are built on, without a word.

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""Write the database clang-tidy reads: compile_db.py [build dir, "out"]."""
import collections
import json
import os
import shlex
import shutil
import subprocess
import sys
GCC_ONLY = frozenset(["-fhardened"])
MSVC_NOISE = frozenset(["MP", "WX", "FS", "showIncludes"])
MSVC_FILES = ("Yu", "Yc", "Fp", "Fo", "Fd")
def tools_directory():
"""Where clang-tidy sits, since its clang++ has to stand in for ours."""
tidy = (os.environ.get("CLANG_TIDY") or shutil.which("clang-tidy")
or shutil.which("clang-tidy", path="/opt/homebrew/opt/llvm/bin"))
if not tidy:
sys.exit("error: clang-tidy not found; set CLANG_TIDY")
return os.path.dirname(tidy)
def compiler_for(original, directory):
"""clang-cl stands in for MSVC, clang++ for every other compiler."""
msvc = os.path.basename(original).lower() in ("cl", "cl.exe")
name = "clang-cl" if msvc else "clang++"
name += ".exe" if os.name == "nt" else ""
beside = os.path.join(directory, name)
return beside if os.path.exists(beside) else name
def split_command(command):
"""Windows quotes by CommandLineToArgvW rules, which shlex cannot read."""
if os.name != "nt":
return shlex.split(command)
if not command.strip():
return [] # an empty line answers with the path of this very process
import ctypes
shell, kernel = ctypes.windll.shell32, ctypes.windll.kernel32
shell.CommandLineToArgvW.restype = ctypes.POINTER(ctypes.c_wchar_p)
count = ctypes.c_int()
parts = shell.CommandLineToArgvW(command, ctypes.byref(count))
try:
return [parts[index] for index in range(count.value)]
finally:
kernel.LocalFree(parts)
def dropped(argument):
"""An option clang rejects or cannot use; /FI stays, it force-includes."""
if argument in GCC_ONLY:
return True
if argument[:1] not in ("/", "-"):
return False
flag = argument[1:]
return flag.startswith(MSVC_FILES) or flag in MSVC_NOISE
def platform_arguments(driver):
"""What clang-tidy's clang needs and the build's own compiler did not."""
if sys.platform == "darwin":
sdk = subprocess.run(["xcrun", "--show-sdk-path"],
capture_output=True, text=True).stdout.strip()
if not sdk:
return []
# Its own libc++ is newer than ours and rejects some of our files.
return ["-isysroot", sdk, "-nostdinc++",
"-isystem" + os.path.join(sdk, "usr/include/c++/v1")]
if os.name == "nt":
# MSVC backs an enum of no fixed type by int, so mtpc_ cases narrow.
return ["-Wno-c++11-narrowing"]
if not os.path.basename(driver).startswith(("gcc", "g++")):
return []
found = shutil.which(driver) or driver
prefix = os.path.dirname(os.path.dirname(found))
if not os.path.isdir(prefix):
print(f"warning: {driver} not on PATH, so the standard library it "
f"carries stays hidden from clang", file=sys.stderr)
return []
return ["--gcc-toolchain=" + prefix]
def rewrite(command):
"""The command without what clang-tidy's clang cannot take."""
arguments, kept, index = split_command(command), [], 0
while index < len(arguments):
argument = arguments[index]
# Only clang's own form: GCC's -include of the header has to stay.
if (argument == "-Xclang"
and arguments[index + 1:index + 2] == ["-include-pch"]):
index += 4
continue
if index and dropped(argument):
index += 1
continue
kept.append(argument)
index += 1
if kept and os.path.basename(kept[0]) in ("ccache", "sccache"):
kept.pop(0)
return kept
def main():
build = sys.argv[1] if len(sys.argv) > 1 else "out"
if not os.path.exists(os.path.join(build, "build.ninja")):
sys.exit(f"error: no ninja build in {build}")
raw = subprocess.run(["ninja", "-C", build, "-t", "compdb"], check=True,
capture_output=True, text=True).stdout
commands = []
for entry in json.loads(raw):
if not entry["file"].endswith((".cpp", ".mm", ".cxx")):
continue
if not entry["command"].strip() or not os.path.exists(entry["file"]):
continue
kept = rewrite(entry["command"])
if kept:
commands.append((entry, kept))
directory = tools_directory()
drivers = collections.Counter(kept[0] for _, kept in commands)
extra = platform_arguments(
drivers.most_common(1)[0][0] if drivers else "")
entries = [{
"directory": entry["directory"],
"file": entry["file"],
"arguments": [compiler_for(kept[0], directory)] + extra + kept[1:],
} for entry, kept in commands]
target = os.path.join(build, "clang-tidy")
os.makedirs(target, exist_ok=True)
with open(os.path.join(target, "compile_commands.json"), "w") as stream:
json.dump(entries, stream, indent=1)
print(f"{len(entries)} entries -> {target}/compile_commands.json")
if __name__ == "__main__":
main()