| #!/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. | |
| ## --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. | |
| ## --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". | |
| ## --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. | |
| ## | |
| ## 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 | |
| upstream="origin/main" | |
| 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" ;; | |
| *.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. | |
| 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 | |
| echo "${file}" | |
| done | |
| done | |
| } | |
| DRY_RUN= | |
| VERBOSE= | |
| CHANGED_LINES= | |
| ALL_FILES=0 | |
| FILES_SPECIFIED=false | |
| fx-config-read | |
| GET_FILES=get_git_files | |
| while [ $# -gt 0 ]; do | |
| ARG="$1" | |
| case "$1" in | |
| --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=: | |
| FILES=$(canonicalize "${PWD}" $(zap-commas "${ARG#--files=}")) | |
| ;; | |
| --target=*) | |
| GET_FILES=: | |
| 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=})) ;; | |
| --) break ;; | |
| *) usage && printf "Unknown flag %s\n" "${ARG}" && exit 1 ;; | |
| esac | |
| shift | |
| done | |
| GIT_FILTER=("$@" :!third_party/rust_crates/vendor/* :!third_party/golibs/vendor/*) | |
| get_git_files() { | |
| FILES=$(canonicalize $(git rev-parse --show-toplevel) \ | |
| $(git diff --name-only $(get-diff-base) "${GIT_FILTER[@]}")) | |
| } | |
| get_all_files() { | |
| FILES=$(canonicalize "${PWD}" $(git ls-files "${GIT_FILTER[@]}")) | |
| } | |
| # 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 | |
| # Do not format these files | |
| NO_FORMAT=( | |
| # fxbug.dev/46090 | |
| "${FUCHSIA_DIR}/build/unification/images/BUILD.gn" | |
| "${FUCHSIA_DIR}/build/unification/tests/host/BUILD.gn" | |
| "${FUCHSIA_DIR}/zircon/system/banjo/BUILD.gn" | |
| "${FUCHSIA_DIR}/zircon/system/fidl/BUILD.gn" | |
| "${FUCHSIA_DIR}/zircon/system/utest/BUILD.gn" | |
| ) | |
| for to_remove in "${NO_FORMAT[@]}"; do | |
| FILES=("${FILES[@]//$to_remove}") | |
| done | |
| if [[ -n "${VERBOSE}" ]]; then | |
| printf "Files to be formatted:\n%s\n" "${FILES[@]}" | |
| fi | |
| PY_TOOL="${FUCHSIA_DIR}/prebuilt/third_party/vpython/vpython3" | |
| PY_TOOL+=" -vpython-interpreter ${PREBUILT_PYTHON3_DIR}/bin/python3.8" | |
| declare -r PY_TOOL | |
| readonly CLANG_CMD="${PREBUILT_CLANG_DIR}/bin/clang-format -style=file -fallback-style=Google -sort-includes -i" | |
| readonly CML_FMT_CMD="${HOST_OUT_DIR}/cmc format --cml --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="${PY_TOOL} ${FUCHSIA_DIR}/prebuilt/third_party/yapf/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 there is a FIDL file to fix, and we don't have a copy of fidl-format, | |
| # generate one. | |
| 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}" | |
| break | |
| 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 | |
| if [[ ALL_FILES -ne 1 ]]; then | |
| printf "File ${file} does not exist and was not formatted\n" | |
| fi | |
| continue | |
| fi | |
| # Format the file | |
| format-file ${file} | |
| done | |
| # 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 |