blob: 8f9ed2f0c0a48918f1206d9129a42ac3e29e29b1 [file] [log] [blame]
#!/bin/bash
# Copyright 2018 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#### CATEGORY=Source tree
### runs source formatters on modified files
## Usage: fx format-code
## [--dry-run] [--verbose] [--all]
## [--files=FILES,[FILES ...]]
## [--target=GN_TARGET]
## [--git] [--changed-lines] [-- PATTERN]
##
## --dry-run Stops the program short of running the formatters
## --all Formats all code in the git repo under the current working
## directory.
## --args Formats the configured build's |args.gn|
## --files Allows the user to specify files. Files are comma separated.
## Globs are dealt with by bash; fx format-code "--files=foo/*" will
## work as expected.
## --target Allows the user to specify a gn target. If the template for the
## given target is defined in a .gni file, the files to format may
## be forwarded to another target, and specifying the target may not
## work. The workaround is to pass the target to which the files
## are forwarded, instead. Specifically, as of January 2023, you
## can only use this with rustc binary and library targets by
## appending ".actual" to them.
## --git The default; it uses `git diff` against the newest parent
## commit in the upstream branch (or against HEAD if no such commit
## is found). Files that are locally modified, staged or touched by
## any commits introduced on the local branch are formatted.
## --changed-lines
## Format changed lines only. Only supported on a subset of languages
## (currently, just C++). Unsupported languages will continue to have
## the entire file formatted. "Changes" are relative to the git
## commit that would be used by "--git".
## --parallel
## Formats all files in the background rather than waiting on each one
## before starting the next.
## WARNING: with this flag enabled, output from multiple formatters
## may be interleaved, and format-code will exit with status 0 even
## if some formatters failed.
## --verbose Print all formatting commands prior to execution.
## -- PATTERN
## For --all or --git, passes along -- PATTERN to `git ls-files`
## to filter what files are affected. For example, to format all
## rust source files use `fx format-code --all -- "*.rs"`
##
## Supported Languages: C, C++, cml, Dart, FIDL, GN, Go, Python, Rust, JSON
set -e
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/../lib/vars.sh || exit $?
function usage() {
fx-command-help
}
function zap-commas() {
printf %s "$1" | tr ',' '\n'
}
function get-diff-base() {
local upstream=$(git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>/dev/null)
if [[ -z "${upstream}" ]]; then
# Make sure upstream is always an ancestor commit.
if git merge-base --is-ancestor origin/main HEAD; then
upstream="origin/main"
elif git merge-base --is-ancestor JIRI_HEAD HEAD; then
if git merge-base --is-ancestor JIRI_HEAD origin/main; then
# You may have some merged CLs in your checkout between JIRI_HEAD and origin/main
upstream=$(git merge-base HEAD origin/main)
else
upstream="JIRI_HEAD"
fi
else
upstream="HEAD"
fi
fi
local local_commit=$(git rev-list HEAD ^${upstream} -- 2>/dev/null | tail -1)
if [[ -z "${local_commit}" ]]; then
printf "HEAD"
else
git rev-parse "${local_commit}"^
fi
}
# Execute a command, printing it first if VERBOSE is set.
function print-and-execute() {
if [[ -n "${VERBOSE}" ]]; then
echo "$@"
fi
"$@"
}
# Format the given C++ file.
function format-cc-file() {
if [[ -n ${CHANGED_LINES} ]]; then
# Only update changed lines.
local changed_line_commands=$(git-print-changed-lines "$1" "--lines=%dF:%dL ")
if [[ -n ${changed_line_commands} ]]; then
print-and-execute ${CLANG_CMD} ${changed_line_commands} "$1"
fi
else
# Update entire files.
print-and-execute ${CLANG_CMD} "$1"
fi
# Unconditionally update header guards.
if [[ $1 =~ .*\.h ]]; then
print-and-execute ${FIX_HEADER_GUARDS_CMD} "$1"
fi
}
# Run the given format command on a single file, warning if "CHANGED_LINES" is set.
function format-full-file() {
local command="$1"
local file="$2"
if [ -n "${CHANGED_LINES}" ]; then
echo "Warning: cannot format only modified lines; formatting full file ${file}" >&2
fi
print-and-execute ${command} "${file}"
}
# Format the given file.
function format-file() {
# If the user requested formatting of a single specific file, we want to
# loudly alert them to the fact that we can't actually format that file, since
# silently exiting would suggest that we *do* know how to format the file and
# that it was already properly formatted.
must_format=false
if $FILES_SPECIFIED && [[ ${#FILES[@]} == 1 ]]; then
must_format=true
fi
filename="$(basename "$1")"
case "${filename}" in
*.c | *.cc | *.cpp | *.h | *.hh | *.hpp | *.proto | *.ts)
format-cc-file "$1" ;;
*.bazel| *.bzl| *.bzlmod| BUILD| WORKSPACE)
format-full-file "${BAZEL_FORMAT_CMD}" "$1" ;;
*.cmx) format-full-file "${JSON_FMT_CMD}" "$1" ;;
*.cml) format-full-file "${CML_FMT_CMD}" "$1";;
*.dart) format-full-file "${DART_CMD}" "$1";;
*.fidl) format-full-file "${FIDL_CMD}" "$1" ;;
*.gidl) format-full-file "${GIDL_CMD}" "$1" ;;
*.gn) format-full-file "${GN_CMD}" "$1" ;;
*.gni) format-full-file "${GN_CMD}" "$1" ;;
*.go) format-full-file "${GO_CMD}" "$1" ;;
*.config) format-full-file "${JSON_FMT_CMD}" "$1" ;;
*.json) format-full-file "${JSON_FMT_CMD}" "$1" ;;
*.json5) format-full-file "${JSON5_FMT_CMD}" "$1" ;;
*.py) format-full-file "${PY_CMD}" "$1" ;;
*.rs) format-full-file "${RUST_FMT_CMD}" "$1" ;;
*.triage) format-full-file "${JSON5_FMT_CMD}" "$1";;
*.persist) format-full-file "${JSON5_FMT_CMD}" "$1";;
*.*)
if $must_format; then
fx-error "Unsupported extension for file $1"
exit 1
fi
;;
*)
if $must_format; then
fx-error "Unable to determine language for file $1"
exit 1
fi
;;
esac
}
# Removes leading //, resolves to absolute path, and resolves globs. The first
# argument is a path prefix, and the remaining arguments are relative to that
# path prefix.
#
# When calling this function, it is usually a good idea to set IFS to something
# that is not likely to be contained in a filename being canonicalized (e.g.,
# IFS=$'\n')
function canonicalize() {
local root_dir="$1"
shift
for fileglob in "${@}"; do
# // means it comes from gn, [^/]* means it is relative
if [[ "${fileglob}" = //* || "${fileglob}" = [^/]* ]]; then
local dir="${root_dir}"/
else
local dir=""
fi
for file in "${dir}""${fileglob#"//"}"; do
printf "${file}\n"
done
done
}
ARGS_GN=
DRY_RUN=
VERBOSE=
CHANGED_LINES=
ALL_FILES=0
FILES_SPECIFIED=false
PARALLEL=
fx-config-read
GET_FILES=get_git_files
while [ $# -gt 0 ]; do
ARG="$1"
case "$1" in
--args) ARGS_GN="1" ;;
--parallel) PARALLEL="1" ;;
--verbose) VERBOSE="1" ;;
--dry-run) DRY_RUN="1" ;;
--changed-lines) CHANGED_LINES="1" ;;
--all)
GET_FILES=get_all_files
ALL_FILES=1
;;
--git)
GET_FILES=get_git_files
ALL_FILES=1
;;
--files=*)
FILES_SPECIFIED=true
GET_FILES=:
OLDIFS=$IFS && IFS=$'\n' \
&& FILES=( \
$(canonicalize "${PWD}" $(zap-commas "${ARG#--files=}")) \
) \
&& IFS=$OLDIFS
;;
--target=*)
GET_FILES=:
OLDIFS=$IFS && IFS=$'\n' \
&& FILES=( \
$(canonicalize "${FUCHSIA_DIR}" \
$(fx-gn desc "${FUCHSIA_BUILD_DIR}" "${ARG#--target=}" sources)) \
) \
&& RUST_ENTRY_POINT=$(canonicalize "${FUCHSIA_DIR}" \
$(fx rustfmt --print-sources ${ARG#--target=})) \
&& IFS=$OLDIFS
;;
--) break ;;
*) usage && printf "Unknown flag %s\n" "${ARG}" && exit 1 ;;
esac
shift
done
GIT_FILTER=(
"$@"
":(top,exclude)third_party/golibs/vendor"
":(top,exclude)third_party/rust_crates"
":(top,exclude)src/devices/tools/fidlgen_banjo/tests"
":(top,exclude)src/devices/tools/fidlgen_banjo/src/backends/templates/rust"
":(exclude)*/goldens/*"
)
get_git_files() {
OLDIFS=$IFS && IFS=$'\n' \
&& FILES=( \
$(canonicalize $(git rev-parse --show-toplevel) \
$(git diff --name-only $(get-diff-base) "${GIT_FILTER[@]}")) \
) \
&& IFS=$OLDIFS
}
get_all_files() {
OLDIFS=$IFS && IFS=$'\n' && FILES=( \
$(canonicalize "${PWD}" $(git ls-files "${GIT_FILTER[@]}")) \
) \
&& IFS=$OLDIFS
}
# Print lines changed in the given file.
#
# "Changes" are calculated relative to `git-diff-base`.
git-print-changed-lines() {
# Have git run `/usr/bin/diff` on the input file.
#
# We in turn ask `diff` to print for each new or modified line the format
# string in `$2`. Details about the format string can be found in the man
# page for diff, under `--new-group-format`.
git difftool -y \
-x "diff --new-group-format='$2' --line-format=''" \
$(get-diff-base) -- "$1"
}
$GET_FILES
if [[ $ARGS_GN == "1" ]]; then
FILES+=( "${FUCHSIA_BUILD_DIR}/args.gn" )
fi
if [[ -n "${VERBOSE}" ]]; then
printf "Files to be formatted:\n%s\n" "${FILES[@]}"
fi
PY_TOOL="fuchsia-vendored-python"
declare -r PY_TOOL
VPYTHON_TOOL="${FUCHSIA_DIR}/prebuilt/third_party/vpython/vpython3"
declare -r VPYTHON_TOOL
readonly BAZEL_FORMAT_CMD="${PREBUILT_BUILDIFIER} -lint=warn"
readonly CLANG_CMD="${PREBUILT_CLANG_DIR}/bin/clang-format -style=file -fallback-style=Google -sort-includes -i"
readonly CMC_TOOL="$( fx-command-run list-build-artifacts --expect-one --name cmc tools )"
readonly CMC_TOOL_ABS="${FUCHSIA_BUILD_DIR}/${CMC_TOOL}"
readonly CML_FMT_CMD="${CMC_TOOL_ABS} format --in-place"
readonly DART_CMD="${PREBUILT_DART_DIR}/bin/dart format"
readonly FIDL_FORMAT_TOOL="$( fx-command-run list-build-artifacts --expect-one --name fidl-format tools )"
readonly FIDL_FORMAT_TOOL_ABS="${FUCHSIA_BUILD_DIR}/${FIDL_FORMAT_TOOL}"
readonly FIDL_CMD="${FIDL_FORMAT_TOOL_ABS} -i"
readonly GIDL_FORMAT_TOOL="$( fx-command-run list-build-artifacts --expect-one --name gidl-format tools )"
readonly GIDL_FORMAT_TOOL_ABS="${FUCHSIA_BUILD_DIR}/${GIDL_FORMAT_TOOL}"
readonly GIDL_CMD="${GIDL_FORMAT_TOOL_ABS} -i"
readonly GN_CMD="${PREBUILT_GN} format"
readonly GO_CMD="${PREBUILT_GO_DIR}/bin/gofmt -s -w"
readonly JSON_FMT_CMD="${PY_TOOL} ${FUCHSIA_DIR}/scripts/style/json-fmt.py --no-sort-keys"
readonly JSON5_FMT_CMD="${HOST_OUT_DIR}/formatjson5 --replace"
readonly MARKDOWN_FMT_TOOL="$(fx-command-run list-build-artifacts --expect-one --name doc-checker tools)"
readonly MARKDOWN_FMT_TOOL_ABS="${FUCHSIA_BUILD_DIR}/${MARKDOWN_FMT_TOOL}"
readonly PY_CMD="${VPYTHON_TOOL} ${FUCHSIA_DIR}/scripts/yapf --in-place"
readonly RUST_ENTRY_POINT_FMT_CMD="${PREBUILT_RUST_DIR}/bin/rustfmt --config-path=${FUCHSIA_DIR}/rustfmt.toml"
readonly RUST_FMT_CMD="${PREBUILT_RUST_DIR}/bin/rustfmt --config-path=${FUCHSIA_DIR}/rustfmt.toml --unstable-features --skip-children"
readonly FIX_HEADER_GUARDS_CMD="${PY_TOOL} ${FUCHSIA_DIR}/scripts/style/check-header-guards.py --fix"
# If a required formatter does not exist, but can be built, build it.
for file in "${FILES[@]}"; do
if [[ ${file} =~ .*\.fidl ]]; then
if [[ ! -x "${FIDL_FORMAT_TOOL_ABS}" ]]; then
fx-info "fidl-format not built; building now..."
fx-command-run build "${FIDL_FORMAT_TOOL}"
fi
elif [[ ${file} =~ .*\.gidl ]]; then
if [[ ! -x "${GIDL_FORMAT_TOOL_ABS}" ]]; then
fx-info "gidl-format not built; building now..."
fx-command-run build "${GIDL_FORMAT_TOOL}"
fi
elif [[ ${file} =~ .*\.cml ]]; then
if [[ ! -x "${CMC_TOOL_ABS}" ]]; then
fx-info "cmc not built; building now..."
fx-command-run build "${CMC_TOOL}"
fi
fi
done
[[ -n "${DRY_RUN}" ]] && exit
[[ -n "${RUST_ENTRY_POINT}" ]] && ${RUST_ENTRY_POINT_FMT_CMD} "${RUST_ENTRY_POINT}"
# Format files.
for file in "${FILES[@]}"; do
# Git reports deleted files, which we don't want to try to format
if [[ ! -f "${file}" ]]; then
continue
fi
# Format the file
if [[ -n "$PARALLEL" ]]; then
format-file "${file}" &
else
format-file "${file}"
fi
done
[[ -n "$PARALLEL" ]] && wait
# If a Markdown change is present (including a deletion of a markdown file),
# check the entire project.
declare HAS_MARKDOWN=
for file in "${FILES[@]}"; do
if [[ ${file} == *.md ]]; then
HAS_MARKDOWN="1";
fi
done
if [[ -n "${HAS_MARKDOWN}" ]]; then
if [[ ! -x "${MARKDOWN_FMT_TOOL_ABS}" ]] ; then
fx-info "doc-checker not built; building now..."
fx-command-run build "${MARKDOWN_FMT_TOOL}"
fi
print-and-execute "${MARKDOWN_FMT_TOOL_ABS}" --local-links-only --root "${FUCHSIA_DIR}";
fi
# The last thing this script does is often the [[ -n "${hgcmd}" ]], which will
# often return a non-zero value. So, we force the script to return 0 and rely
# on "set -e" to catch real errors.
exit 0