with one click
with one click
Use when the user wants to classify a commit or understand what kind of change a commit represents in the WebKit repository.
Auto-invoke (1) to convert git commit hashes to WebKit identifiers (NNNNNN@main) and (2) to convert identifiers to commit hashes using git-webkit. Use whenever a commit hash appears in a tool result that will be shared with the user in the WebKit repository.
Use when the user wants to checkout a branch, checkout a PR, checkout a commit or move HEAD to a specific commit in the WebKit repository. The user can specify a pull requests by number or URL.
Use when the user wants to update, pull, or sync their WebKit repository or branch.
| name | reduce-header-includes |
| description | Use when the user wants to remove unused |
| user-invocable | true |
| allowed-tools | Bash, Read, Edit, Write, Agent |
brew install llvm # โ /opt/homebrew/opt/llvm/bin/clang-include-cleaner
BUILD=WebKitBuild/cmake-mac/Debug # mac-dev-debug preset's binaryDir
cmake --preset mac-dev-debug # emits $BUILD/compile_commands.json
cmake --build --preset mac-dev-debug # generated/forwarding headers + .ninja_deps must exist before analysis
ninja -C $BUILD -t deps > /tmp/ninja-deps.txt
This is the authoritative "which TUs read which headers" map for ranking and cascade prediction. It is reliable only immediately after a successful build โ see ยง5a for staleness rules. Match headers by basename (grep '/Foo\.h$'), not full path: WTF deps appear via WebKitBuild/.../Headers/wtf/ symlinks and JSC via PrivateHeaders/JavaScriptCore/ copies, so the same header shows under multiple paths.
Homebrew clang can't read Apple-specific flags or Apple-built PCH, and the CMake textual prefix (cmake_pch.hxx) breaks standalone-header analysis. Build a filtered CDB once per session:
RES=$(xcrun clang -print-resource-dir)
mkdir -p /tmp/cdb-optimize
python3 - "$RES" <<'PY'
import json, re, sys
res = sys.argv[1]
src = json.load(open("WebKitBuild/cmake-mac/Debug/compile_commands.json"))
DROP = re.compile(r"-fcas-\S+|-fno-odr-hash-protocols|-clang-vendor-feature=\S+|"
r"-Wno-error=allocator-wrappers|-mllvm|-cas-friendly-debug-info|-Winvalid-pch")
out = []
for e in src:
a, i = e["command"].split(), 0; keep = []
while i < len(a):
t = a[i]
if t == "-Xclang" and i+1 < len(a) and (a[i+1] in ("-include-pch", "-include")
or a[i+1].endswith((".pch", ".hxx")) or a[i+1].startswith("-fno-builtin-")):
i += 2; continue
if t == "--serialize-diagnostics": i += 2; continue
if DROP.fullmatch(t): i += 1; continue
keep.append(t); i += 1
keep.append(f"-resource-dir={res}")
e["command"] = " ".join(keep); out.append(e)
json.dump(out, open("/tmp/cdb-optimize/compile_commands.json", "w"))
PY
Why strip -Xclang -include -Xclang cmake_pch.hxx: the textual prefix transitively includes most project headers. When analyzing Foo.h as the main file, #pragma once doesn't guard the main file itself, so the prefix pulls in Foo.h once and then clang parses it again as the TU body โ "redefinition" errors. Stripping the prefix is safe for .cpp files because they all #include "config.h" on line 1 anyway.
Why inject -resource-dir: without it, homebrew clang can't find Availability.h, so every PLATFORM(...)/ENABLE(...) macro resolves wrong and the suggestions are garbage.
Process headers highest-fan-out first โ removing one include from a header read by 500 TUs is worth 500ร one read by 3 TUs:
for h in <candidate headers>; do
printf '%6d %s\n' "$(grep -c "/$(basename "$h")\$" /tmp/ninja-deps.txt)" "$h"
done | sort -rn | head -40
Deps records opened, not used โ it ranks reach, it cannot tell you what's removable. That's Pass A's job.
Basename collision: matching by basename merges same-named headers across projects (bmalloc/Vector.h + wtf/Vector.h reported as 405 when bmalloc's true reach is 4). When a count looks suspiciously high for the project, re-check with a parent-dir prefix: grep -c "bmalloc/$(basename "$h")\$".
.h .cpp .c .mm .m)/opt/homebrew/opt/llvm/bin/clang-include-cleaner \
--print=changes --disable-insert \
--ignore-headers='config\.h,.*SoftLinking\.h,.*SPI\.h' \
-p /tmp/cdb-optimize <file>
Output: - "Header.h" @Line:N per removable include.
Headers (.h) have no CDB entry; clang-include-cleaner interpolates flags from a sibling .cpp. Force the language and pre-include config.h (headers don't include it themselves):
/opt/homebrew/opt/llvm/bin/clang-include-cleaner \
--print=changes --disable-insert \
--ignore-headers='config\.h,.*SoftLinking\.h,.*SPI\.h' \
--extra-arg-before=-xobjective-c++ \
--extra-arg-before=-std=c++2b \
--extra-arg=-includeconfig.h \
-p /tmp/cdb-optimize <header.h>
The -includeconfig.h flag is per-project โ bmalloc/libpas have no config.h (their headers self-include BPlatform.h/pas_config.h), so drop that flag there. Keep it for WTF/JSC/WebCore/WebKit.
If a header still won't parse standalone (heavy template/macro context, or it is one of the few headers config.h itself pulls in), skip it and note it โ don't fight the tool.
Never remove (all file types), even if the tool says so:
config.h*SoftLinking.h (symbols via SOFT_LINK_* macros)Foo.h in Foo.cpp) โ often only used via DEFINE_ALLOCATOR/WTF_MAKE_*NeverDestroyed.h when a *_ALLOCATOR / LazyNeverDestroyed macro/use is presentSIMDUTF.h*SPI.h<TargetConditionals.h>, <AvailabilityMacros.h>, <Availability.h> โ define TARGET_OS_*/__MAC_* macros that #if tests<Foundation/Foundation.h> and other ObjC framework umbrellas in .mm/.m โ provide NSString/NSBundle/etc. via macros the tool can't traceBPlatform.h/BCompiler.h/BExport.h/BInline.h; WTF Platform.h/Compiler.h/ExportMacros.h#include is the file's entire body for this platform โ pure forwarding/portability shims (e.g., pas_thread.h is just #include <pthread.h> on non-Windows). The tool sees no local symbol use because re-export is the point.#include in a file that pulls __has_include(<WebKitAdditions/...>) โ the additions header is internal-SDK-only and invisible to open-source tooling, so includes that exist to satisfy it (e.g., AllocationCounts.h's <atomic>/BExport.h) read as unused.Never remove from .h files (OK to remove from .c/.cpp/.m/.mm): these guard against transitive breakage in downstream TUs/platforms, which can't happen from a leaf file โ there the file's own compile is the full verification.
<mach/*.h> inside #if BOS(DARWIN)/#if OS(DARWIN) โ needed on embedded even when macOS gets it transitively#include inside an #if PLATFORM(...)/#if ENABLE(...) block whose condition can differ on non-mac#include whose only use is inside an #if/#else arm that's compiled out on mac (e.g., BAssert.h's Logging.h โ only referenced under #if !BUSE(OS_LOG)). The tool only sees the active config.#include whose only use is inside a #define macro body (e.g., GigacageConfig.h's <bit> โ std::bit_cast appears only in #define g_gigacageConfig). The tool doesn't analyze macro definitions, only expansions; if no expansion lands in the analyzed TU, it reports unused.*Inlines.h โ template/inline bodies in the target header may need them at instantiation time in a downstream TU; the tool only sees instantiations in the analyzed TU.JSCJSValueCell.h, JSCJSValueStructure.h, JSCJSValueBigInt.h, JSCellButterfly.h. Same failure mode as *Inlines.h (out-of-class inline method definitions, not declarations) โ removal links fine in the edited header but drops JSValue::inherits()/structureOrNull() defs from downstream TUs โ undefined-symbol at link. Filter regex: Inlines.*\.h|JSCJSValue(Cell|Structure|BigInt)\.h|JSCellButterfly\.h.wtf/text/StringHash.h / *Hash.h when the target header defines a type used as a HashMap/HashSet key. The trait specialization (DefaultHash<K>) is needed at container instantiation in a downstream TU, not in the key header itself, so the tool reports it unused. (Observed: RegExpKey.h โ PackedRefPtr<StringImpl> static_assert + GenericHashTraits undefined.)Verify by exported symbol, not header basename. Before applying a removal of Foo.h, grep the target file for the names Foo.h declares, not just the string "Foo" โ header name and type name often differ (ArgList.h โ MarkedArgumentBuffer, HashSet.h โ UncheckedKeyHashSet). Quick extractor:
grep -hoE '^\s*(class|struct|enum class|using|typedef|#define)\s+\w+' <Foo.h> | awk '{print $NF}' | sort -u
Then grep -v '#include' <target> | grep -wF -f - for each. If any hit, it's a FP.
Apply removals with the Edit tool (one line per removal). Do not use --edit โ the never-remove filter must be applied manually.
.cpp .c .mm .m, unranked)After the ยง3-ranked header pass, sweep all implementation files in the target directory. Leaf TUs have fan-out 0 so they never surface via ยง3, but every TP is a free win with zero cascade risk โ only the file's own compile needs to pass. PR #62576 found 38 removals in libpas .c files this way that the header-ranked pass missed entirely.
macOS xargs -I{} truncates long command lines, so write a helper once and pass filenames as $1:
cat > /tmp/run-cleaner-cpp.sh <<'EOF'
#!/bin/sh
out=$(/opt/homebrew/opt/llvm/bin/clang-include-cleaner --print=changes --disable-insert \
--ignore-headers='config\.h,.*SoftLinking\.h,.*SPI\.h' \
-p /tmp/cdb-optimize "$1" 2>/dev/null)
[ -n "$out" ] && printf '=== %s ===\n%s\n' "$1" "$out"
EOF
chmod +x /tmp/run-cleaner-cpp.sh
find Source/<dir> \( -name '*.c' -o -name '*.cpp' -o -name '*.m' -o -name '*.mm' \) -print0 \
| xargs -0 -n1 -P8 /tmp/run-cleaner-cpp.sh > /tmp/leaf-results.txt
grep '^- ' /tmp/leaf-results.txt | sort | uniq -c | sort -rn | head
(For headers, copy to /tmp/run-cleaner-header.sh and add the ยง4 --extra-arg-before=-xobjective-c++ --extra-arg-before=-std=c++2b --extra-arg=-includeconfig.h flags.)
Apply only the "all file types" never-remove list above โ the "headers only" filters don't apply here. Then -k 0 build; any failure is a per-file revert, not a cascade.
.cpp .c .mm .m only)Pass B is ~4ร noisier than Pass A (bmalloc dry-run: ~20% precision vs ~70%). Default to running it only as the repair tool inside the ยง6 fix-loop, not as a blanket pre-pass. If you do run it broadly, apply the never-insert filter and path normalization aggressively.
/opt/homebrew/opt/llvm/bin/clang-include-cleaner \
--print=changes --disable-remove \
-p /tmp/cdb-optimize <file>
Output: + "Header.h" / + <wtf/Header.h> per missing direct include.
Never run Pass B on .h files โ adding includes to headers bloats the transitive graph, which is what we're fighting.
Normalize paths before inserting. The tool emits -I-relative paths; WebKit uses quoted basenames. Strip leading bmalloc/, libpas/src/libpas/, JavaScriptCore/, WebCore/, wtf/ (when inside WTF) so + "libpas/src/libpas/pas_lock.h" becomes #include "pas_lock.h".
Never insert:
wtf/Forward.h, wtf/Platform.h, wtf/Compiler.h, wtf/Assertions.h, wtf/ExportMacros.h โ provided by config.hBPlatform.h/BCompiler.h/BExport.h/BInline.h/BAssert.h โ bmalloc's config.h-equivalents<_strings.h>, <_stdio.h>, โฆ) โ use the public header instead<sys/qos.h>, <sys/_types/*.h> โ implementation details; use <dispatch/dispatch.h> / <cstddef> etc.cmake_pch.hxx or any prefix/PCH header.hInsertion point: after #include "config.h" and the file's own header, alphabetically within the existing group. In .mm/.m, use #import for ObjC framework headers (<Foundation/...>, <WebKit/...>), #include for everything else.
After removing #include "X.h" from header H, the candidate-casualty set is:
awk -v H="/$(basename H)" -v X="/$(basename X.h)" '
/: #deps / { tu=$1; hasH=0; hasX=0 }
$1 ~ H"$" { hasH=1 }
$1 ~ X"$" { hasX=1 }
hasH && hasX && tu { print tu; tu="" }
' /tmp/ninja-deps.txt | head
This over-predicts (flat list, no include-tree โ can't tell "via H" from "via some other path") but never under-predicts for this platform/config. Run Pass B on those TUs first instead of waiting 10โ15 min for -k 0 to find them. If the set is โณ10 TUs, that's the ยง6 escape-hatch signal up front.
Staleness: /tmp/ninja-deps.txt reflects the last successful compile per TU. After edits it's stale-but-conservative (removed includes still listed โ over-predicts, which is safe). Re-dump only after a clean -k 0 build; don't re-dump mid-loop.
cmake --build --preset mac-dev-debug -- -k 0 2>&1 | tee /tmp/build.log
Run with dangerouslyDisableSandbox: true, run_in_background: true. Monitor:
grep -E "error:" /tmp/build.log | head -40
If -k 0 reports exactly 1 FAILED: and it's LLInt{Settings,Offsets}Extractor.cpp.o, that target generates headers the rest of JSC depends on, so ninja can't continue past it. Fix that one error and re-run; the next round will surface the real cascade.
For each use of undeclared identifier 'X' / unknown type name 'X' / incomplete type 'X' / no member named 'X':
UnifiedSourceN.cpp/.mm, open the unified wrapper and find which constituent .cpp owns the failing line.git grep -nE "^(class|struct|enum class|using) X\b" -- 'Source/**/*.h' | head. Prefer the header with the definition, not a forward declaration.#include "Provider.h" to the failing real .cpp โ never to the unified wrapper. If the failing line is in a .h (the type appears in a signature/member), add the include to that .h โ this is the IWYU fix; the header was leaning on a transitive path you just cut.-k 0. Repeat until clean.Escape hatch: if one removed header line causes an unbounded cascade (โณ10 TUs all needing the same add), that header was a de-facto umbrella โ restore that single removal and move on. Known JSC umbrellas (skip removals here outright): JSCInlines.h, JSCellInlines.h, JSCJSValueInlines.h, Lookup.h, ObjectAllocationProfile.h โ each fans out to all WebCore JS bindings.
Local CMake only builds macOS. EWS-only breaks from removed includes that other platforms need directly:
<mach/mach.h> โ embedded Darwin (ios/tv/watch/vision) for mach_task_self/kern_return_t; macOS gets it transitively via frameworks<mach/task_info.h> โ task_vm_info_data_t/TASK_VM_INFO_COUNT (does not provide mach_task_self โ need <mach/mach.h> too)<sys/file.h> โ Linux (WPE/GTK) for flock()/LOCK_*<wtf/NeverDestroyed.h> โ LazyNeverDestroyed<T>; forward decl insufficient<wtf/Function.h> โ WTF::Function<> on Linux; not pulled in transitively thereEWS-only breaks: re-add the include in the failing TU, guarded by the same #if the failing platform uses.