187 lines
7.5 KiB
Bash
187 lines
7.5 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# xo-clang-format-includes - enforce the xo include-ordering policy, includes-only
|
|
#
|
|
# clang-format has no "sort includes but touch nothing else" mode: a style
|
|
# governs the WHOLE file, so `clang-format -i` would also reindent/rebrace code.
|
|
# This wrapper confines clang-format to each file's leading #include block via
|
|
# `--lines`, so only include statements are ever rewritten.
|
|
#
|
|
# It is opt-in and self-contained: it passes the style explicitly
|
|
# (--style=file:...), so it needs NO root .clang-format -- and during the
|
|
# interim there should NOT be one, or editors/IDEs would reformat whole files
|
|
# against a mostly-default style.
|
|
#
|
|
# Usage:
|
|
# xo-clang-format-includes [--check|--fix] [--build DIR | --style FILE] FILE...
|
|
# xo-clang-format-includes [--check|--fix] [--build DIR | --style FILE] --changed [REF]
|
|
#
|
|
# --check (default) report files whose includes violate policy; exit 1 if any
|
|
# --diff show the unified diff --fix would make, WITHOUT applying it; exit 1 if any
|
|
# --fix rewrite includes in place
|
|
# --build build directory (as passed to cmake -B): use DIR/dot-clang-format,
|
|
# the candidate cmake regenerates from the live subsystem graph
|
|
# --style explicit dot-clang-format to use (overrides --build)
|
|
# --changed operate on C/C++ files changed vs REF (default: HEAD)
|
|
# -R DIR recurse DIR, operating on every *.hpp / *.cpp under it
|
|
# --normalize-brackets (DEFAULT) rewrite cross-repo "xo/other/..." ->
|
|
# <xo/other/...> before ordering (a file in xo-foo/ keeps
|
|
# "xo/foo/..." quoted). On by default because WITHOUT it, quoted xo
|
|
# includes sort as same-repo (alphabetical) and can REGRESS a
|
|
# correctly topo-ordered file; clang-format can't rewrite delimiters.
|
|
# --no-normalize-brackets
|
|
# disable the above: order includes as-is, leaving delimiters alone.
|
|
#
|
|
# FILE..., -R DIR, and --changed accumulate; pass any combination.
|
|
#
|
|
# Style resolution, first that applies: --style, then --build/dot-clang-format,
|
|
# then $XO_DOT_CLANG_FORMAT, then the installed snapshot.
|
|
|
|
set -euo pipefail
|
|
|
|
STYLE_DEFAULT=@CMAKE_INSTALL_FULL_DATADIR@/etc/xo/dot-clang-format
|
|
|
|
mode=check
|
|
style=""
|
|
build=""
|
|
changed=0
|
|
normalize=1 # cross-repo bracket normalization on by default (opt-out)
|
|
ref=HEAD
|
|
files=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--check) mode=check; shift;;
|
|
--diff) mode=diff; shift;;
|
|
--fix) mode=fix; shift;;
|
|
--normalize-brackets) normalize=1; shift;;
|
|
--no-normalize-brackets) normalize=0; shift;;
|
|
--build) build="$2"; shift 2;;
|
|
--build=*) build="${1#--build=}"; shift;;
|
|
--style) style="$2"; shift 2;;
|
|
--style=*) style="${1#--style=}"; shift;;
|
|
--changed) changed=1; shift
|
|
if [[ $# -gt 0 && "$1" != -* ]]; then ref="$1"; shift; fi;;
|
|
-R) [[ $# -ge 2 ]] || { echo "$0: -R needs a DIR" >&2; exit 2; }
|
|
[[ -d "$2" ]] || { echo "$0: -R: not a directory: $2" >&2; exit 2; }
|
|
mapfile -t _rfiles < <(find "$2" -type f \( -name '*.hpp' -o -name '*.cpp' \))
|
|
files+=("${_rfiles[@]}")
|
|
shift 2;;
|
|
-h|--help) sed -n '3,38p' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
|
|
--) shift; files+=("$@"); break;;
|
|
-*) echo "$0: unknown option: $1" >&2; exit 2;;
|
|
*) files+=("$1"); shift;;
|
|
esac
|
|
done
|
|
|
|
# style resolution: --style wins, then --build/dot-clang-format (the candidate
|
|
# regenerated from the live graph), then $XO_DOT_CLANG_FORMAT, then installed.
|
|
if [[ -z "$style" ]]; then
|
|
if [[ -n "$build" ]]; then
|
|
style="$build/dot-clang-format"
|
|
else
|
|
style="${XO_DOT_CLANG_FORMAT:-$STYLE_DEFAULT}"
|
|
fi
|
|
fi
|
|
if [[ ! -r "$style" ]]; then
|
|
echo "$0: style file not found: $style" >&2
|
|
if [[ -n "$build" ]]; then
|
|
echo " (from --build $build; has it been configured with cmake?)" >&2
|
|
else
|
|
echo " pass --build DIR or --style FILE, set \$XO_DOT_CLANG_FORMAT, or install xo-cmake" >&2
|
|
fi
|
|
exit 2
|
|
fi
|
|
|
|
if [[ $changed -eq 1 ]]; then
|
|
mapfile -t _changed < <(git diff --name-only "$ref" -- '*.cpp' '*.hpp' '*.h' '*.cc' '*.hh')
|
|
files+=("${_changed[@]}")
|
|
fi
|
|
|
|
if [[ ${#files[@]} -eq 0 ]]; then
|
|
echo "$0: no files (pass FILE... or --changed)" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Line range of the leading #include block: first #include through the last
|
|
# #include reachable across only blank / comment / preprocessor lines (stop at
|
|
# the first line of real code). Empty if the file has no top-level includes.
|
|
include_range() {
|
|
awk '
|
|
/^[[:space:]]*#[[:space:]]*include/ { if (!first) first=NR; last=NR; next }
|
|
first && $0 ~ /^[[:space:]]*($|#|\/\/|\/\*|\*)/ { next } # blank/comment/preproc: keep scanning
|
|
first { exit } # first real code line: stop
|
|
END { if (first) print first ":" last }
|
|
' "$1"
|
|
}
|
|
|
|
# Subsystem of a file, from its path: last "xo-<name>/" component, minus "xo-".
|
|
# e.g. .../xo-process/src/process/UpxEvent.cpp -> process
|
|
subsys_of() {
|
|
printf '%s' "$1" | grep -oE 'xo-[^/]+' | tail -1 | sed 's/^xo-//' || true
|
|
}
|
|
|
|
# Rewrite cross-repo include brackets: #include "xo/BAR/..." -> <xo/BAR/...>,
|
|
# EXCEPT keep the file's own subsystem quoted (a file in xo-foo/ may write
|
|
# "xo/foo/..."). Same-repo bare quotes ("Foo.hpp", "detail/...") are untouched.
|
|
AWK_NORMALIZE='
|
|
{
|
|
line = $0
|
|
if (line ~ /^[[:space:]]*#[[:space:]]*include[[:space:]]*"xo\//) {
|
|
q1 = index(line, "\""); rest = substr(line, q1 + 1); q2 = index(rest, "\"")
|
|
path = substr(rest, 1, q2 - 1); split(path, p, "/"); bar = p[2]
|
|
if (bar == FOO)
|
|
print line
|
|
else
|
|
print substr(line, 1, q1 - 1) "<" path ">" substr(rest, q2 + 1)
|
|
} else {
|
|
print line
|
|
}
|
|
}'
|
|
|
|
# Fully processed content of a file (to stdout): optional bracket-normalize,
|
|
# then include ordering (clang-format scoped to the include block via --lines).
|
|
# When normalizing we feed clang-format on stdin, so --assume-filename keeps its
|
|
# main-include (self header) detection working.
|
|
formatted_stream() { # $1 = file, $2 = line range
|
|
if [[ $normalize -eq 1 ]]; then
|
|
awk -v FOO="$(subsys_of "$1")" "$AWK_NORMALIZE" "$1" \
|
|
| clang-format --assume-filename="$1" --style="file:$style" --lines="$2"
|
|
else
|
|
clang-format --style="file:$style" --lines="$2" "$1"
|
|
fi
|
|
}
|
|
|
|
rc=0
|
|
for f in "${files[@]}"; do
|
|
[[ -f "$f" ]] || continue
|
|
rng=$(include_range "$f")
|
|
[[ -z "$rng" ]] && continue # no includes to order
|
|
|
|
case "$mode" in
|
|
fix)
|
|
tmp=$(mktemp)
|
|
# Only write when the content actually differs. Writing
|
|
# unconditionally would bump every file's mtime on every run, and
|
|
# make/ninja key off mtime -- so a no-op --fix over the tree would
|
|
# force a full rebuild.
|
|
if formatted_stream "$f" "$rng" > "$tmp" && [[ -s "$tmp" ]] \
|
|
&& ! cmp -s "$tmp" "$f"; then
|
|
cat "$tmp" > "$f" # preserve $f's inode / permissions
|
|
fi
|
|
rm -f "$tmp"
|
|
;;
|
|
diff)
|
|
diff -u --label "a/$f" --label "b/$f" \
|
|
"$f" <(formatted_stream "$f" "$rng") || rc=1
|
|
;;
|
|
check)
|
|
if ! diff -q "$f" <(formatted_stream "$f" "$rng") >/dev/null; then
|
|
echo "include-order: $f" >&2
|
|
rc=1
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
exit $rc
|