From 52b50714c218743850d9fda2bbf30cd6e732bfef Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 14 Sep 2026 16:22:17 +0300 Subject: [PATCH] Added clang-tidy ban of qMin, qMax, qAbs and qBound. --- .clang-tidy | 12 +++ tools/clangtidy/README.md | 13 ++++ tools/clangtidy/compile_db.py | 137 ++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 .clang-tidy create mode 100644 tools/clangtidy/README.md create mode 100644 tools/clangtidy/compile_db.py diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000000..a95105d40f --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,12 @@ +# Qt helpers where the standard is exact; the list grows one check at a time. +Checks: '-*,bugprone-unsafe-functions' +WarningsAsErrors: 'bugprone-unsafe-functions' +CheckOptions: + bugprone-unsafe-functions.CustomFunctions: > + ^::qMin$,std::min,; + ^::qMax$,std::max,; + ^::qAbs$,std::abs,; + ^::qBound$,std::clamp,orders arguments as (low, value, high) + +# Header findings repeat per translation unit, and submodules are not ours. +HeaderFilterRegex: '' diff --git a/tools/clangtidy/README.md b/tools/clangtidy/README.md new file mode 100644 index 0000000000..5312d55742 --- /dev/null +++ b/tools/clangtidy/README.md @@ -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 +``` + +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. diff --git a/tools/clangtidy/compile_db.py b/tools/clangtidy/compile_db.py new file mode 100644 index 0000000000..16a99ac4ad --- /dev/null +++ b/tools/clangtidy/compile_db.py @@ -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()