|  | #!/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.  Prefer origin/main, because it | 
|  | # is always newer than JIRI_HEAD. | 
|  | if git merge-base --is-ancestor origin/main HEAD; then | 
|  | upstream="origin/main" | 
|  | elif git merge-base --is-ancestor JIRI_HEAD HEAD; then | 
|  | upstream="JIRI_HEAD" | 
|  | 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 |