| #!/bin/bash | |
| # Copyright 2019 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 linters on modified files | |
| ## Usage: fx lint | |
| ## [--dry-run] [--verbose] [--fix] | |
| ## [--all] | |
| ## [--files=FILES,[FILES ...]] | |
| ## [--target=GN_TARGET] | |
| ## [--git] [-- PATTERN] | |
| ## | |
| ## --fix If supported by the linter tool for the target language, | |
| ## attempt to apply recommended fixes | |
| ## --dry-run Print the linter commands but don't run them | |
| ## --verbose Print the linter commands before running them | |
| ## --all Lints all code in the git repo under the current working | |
| ## directory. | |
| ## --files Allows the user to specify files separated by commas or (with | |
| ## quotes) whitespace. Basic globs are supported, for example: | |
| ## fx lint --files=foo/* | |
| ## Or for more advanced filename pattern matching, you can use | |
| ## double quotes with command substitution: | |
| ## fx lint --files="$(find some/path -name '*.cc')" | |
| ## --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 | |
| ## linted. | |
| ## -- [PATTERN...] -additional -args -for -linter | |
| ## For --all or --git, passes along PATTERNs to `git ls-files` | |
| ## to filter what files are affected. The first argument starting | |
| ## with a dash, and all arguments thereafter, are passed to the | |
| ## linter command (positioned before the filename). Used in | |
| ## combination, the pattern can constrain which linter is selected, | |
| ## so linter options are only applied to the expected linter | |
| ## program. | |
| ## | |
| ## Examples: | |
| ## fx lint -- "*.fidl" --include-check no-trailing-comment | |
| ## fx lint --files=sdk/fidl/fuchsia.sys/service_provider.fidl \ | |
| ## -- --exclude-check string-bounds-not-specified | |
| ## fx lint --verbose --files="$(echo sdk/**/*.cc sdk/**/*.fidl)" | |
| 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 | |
| } | |
| # 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= | |
| FIX_OPTION= | |
| TARGET= | |
| 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" ;; | |
| --fix) FIX_OPTION="-fix" ;; | |
| --all) GET_FILES=get_all_files ;; | |
| --git) GET_FILES=get_git_files ;; | |
| --files=*) | |
| GET_FILES=: | |
| FILES=$(canonicalize "${PWD}" $(zap-commas "${ARG#--files=}")) | |
| ;; | |
| --target=*) | |
| GET_FILES=: | |
| >&2 echo "Looking up files for target ${ARG#--target=}..." | |
| declare -i status=0 | |
| GN_OUTPUT=( | |
| $(fx-gn desc "${FUCHSIA_BUILD_DIR}" "${ARG#--target=}" sources) | |
| ) || status=$? | |
| # Hacky, but we need to be able to continue when the target is an action, | |
| # because Rust and Go targets don't list their files. | |
| if [[ ${status} != 0 && "${GN_OUTPUT}" == *"action"* ]]; then | |
| >&2 echo "${GN_OUTPUT[@]}" | |
| exit ${status} | |
| fi | |
| FILES=$(canonicalize "${FUCHSIA_DIR}" "${GN_OUTPUT[@]}") | |
| TARGET="${ARG#--target=}" | |
| ;; | |
| --) | |
| shift | |
| break | |
| ;; | |
| --help) usage && exit 0 ;; | |
| *) usage && fx-error "Unknown flag '${ARG}'\n" && exit 1 ;; | |
| esac | |
| shift | |
| done | |
| GIT_FILTER=() | |
| while [ $# -gt 0 ]; do | |
| if [[ $1 == -* ]]; then | |
| break | |
| fi | |
| if [[ ${#GIT_FILTER[@]} == 0 ]]; then | |
| GIT_FILTER=(--) | |
| fi | |
| GIT_FILTER+=($1) | |
| shift | |
| done | |
| COMMAND_ARGS=("$@") | |
| 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[@]}")) | |
| } | |
| $GET_FILES | |
| # Specify both the clang-tidy and the clang-apply-replacements binaries to be | |
| # the ones in //prebuilt, so that they'll work properly on the tree. | |
| declare CLANG_TIDY="${PREBUILT_CLANG_DIR}/bin/clang-tidy" | |
| declare CLANG_APPLY_REPLACEMENTS="${PREBUILT_CLANG_DIR}/bin/clang-apply-replacements" | |
| declare RUN_CLANG_TIDY="${PREBUILT_CLANG_DIR}/bin/run-clang-tidy" | |
| # Suppress the "name-repeats-*" checks for now. These produce a high number of | |
| # linter warnings, many of which are questionable. The algorithm will be refined. | |
| declare FIDL_LINT_TARGET="$( fx-command-run list-build-artifacts --expect-one --name fidl-lint tools )" | |
| declare FIDL_LINT="${FUCHSIA_BUILD_DIR}/${FIDL_LINT_TARGET}" | |
| declare CLANG_TIDY_CMD=( | |
| "${PREBUILT_PYTHON3_DIR}/bin/python3.8" | |
| "${RUN_CLANG_TIDY}" | |
| -clang-tidy-binary "${CLANG_TIDY}" | |
| -clang-apply-replacements-binary "${CLANG_APPLY_REPLACEMENTS}" | |
| ) | |
| if [[ -n "${FIX_OPTION}" ]]; then | |
| CLANG_TIDY_CMD+=( ${FIX_OPTION} ) | |
| fi | |
| declare FIDL_LINT_CMD=( "${FIDL_LINT}" ) | |
| # TODO(fxbug.dev/27283): add command and args for Dart linter | |
| declare DART_LINT_CMD=() # "${FUCHSIA_DIR}/prebuilt/third_party/dart/${HOST_PLATFORM}/bin/dart<cmd?> <options?>" | |
| # TODO(fxbug.dev/27269): add command and args for Go linter | |
| declare GO_LINT_CMD=() # "${PREBUILT_GO_DIR}/bin/go<cmd?> <options?>" | |
| # TODO(fxbug.dev/27270): add command and args for JSON linter | |
| declare JSON_LINT_CMD=() # "${FUCHSIA_DIR}"/scripts/style/json-<cmd?> <options?>" | |
| declare RUST_LINT_CMD=( "${FUCHSIA_DIR}/tools/devshell/contrib/lib/rust/run-clippy.py" ) | |
| if [[ -n "${VERBOSE}" ]]; then | |
| RUST_LINT_CMD+=( "-v" ) | |
| fi | |
| declare CLANG_TIDY_FILES=() | |
| declare DART_LINT_FILES=() | |
| declare FIDL_LINT_FILES=() | |
| declare GO_LINT_FILES=() | |
| declare JSON_LINT_FILES=() | |
| declare RUST_LINT_FILES=() | |
| function add_file_to_linter() { | |
| local file="$1" | |
| # Git reports deleted files, which we don't want to try to lint | |
| if [ -f "${file}" ]; then | |
| case "${file}" in | |
| *.c) CLANG_TIDY_FILES+=(${file}) ;; | |
| *.cc) CLANG_TIDY_FILES+=(${file}) ;; | |
| *.cmx) JSON_LINT_FILES+=(${file}) ;; | |
| *.cpp) CLANG_TIDY_FILES+=(${file}) ;; | |
| *.dart) DART_LINT_FILES+=(${file}) ;; | |
| *.fidl) FIDL_LINT_FILES+=(${file}) ;; | |
| *.go) GO_LINT_FILES+=(${file}) ;; | |
| *.h) CLANG_TIDY_FILES+=(${file}) ;; | |
| *.hh) CLANG_TIDY_FILES+=(${file}) ;; | |
| *.hpp) CLANG_TIDY_FILES+=(${file}) ;; | |
| # *.rs) RUST_LINT_FILES+=(${file}) ;; # run-clippy.py is not working (fxbug.dev/72865) | |
| esac | |
| # Note: clang-tidy can produce errors on header files (*.h, *.hh, *.hpp) | |
| # clang-tidy uses a compilation database that only contains implementation | |
| # files, and so it makes a best-effort guess at what the relevant | |
| # implementation file is based on the name. However, if you have a header | |
| # with no implementation file or with a different name than the | |
| # implementation file (e.g. "functions.h" and "function_a.cpp" that | |
| # implements it), clang-tidy will return an error. | |
| fi | |
| } | |
| fidl_lint_is_outdated() { | |
| local yes=0 | |
| local no=1 | |
| if [[ ! -x "${FIDL_LINT}" ]]; then | |
| # "fx build zircon/tools" does not build the "${FIDL_LINT}" hard link | |
| # if that is the only missing hard link to the architecture-specific | |
| # executable. Touching these files in this order will ensure the | |
| # executable is rebuilt and relinked. | |
| touch "${FIDL_LINT}" | |
| touch "${FUCHSIA_DIR}/tools/fidl/fidlc/linter/main.cc" | |
| return $yes | |
| fi | |
| local files_out_of_date=$( | |
| find "${FUCHSIA_DIR}/tools/fidl/fidlc" -newer "${FIDL_LINT}" | wc -l | |
| ) | |
| if (( ${files_out_of_date} > 0 )); then | |
| return $yes | |
| fi | |
| return $no | |
| } | |
| ensure_fidl_lint() { | |
| if [[ -t 0 ]]; then # only if interactive | |
| # If there is a FIDL file to check, and we don't have an up-to-date | |
| # copy of fidl-lint, generate one. | |
| for file in ${FILES}; do | |
| if [[ ${file} =~ .*\.fidl ]]; then | |
| if fidl_lint_is_outdated; then | |
| # echo directly to /dev/tty in case stdout and/or stderr are redirected | |
| fx-warn "fidl-lint is out of date or missing..." >/dev/tty | |
| echo -n "OK to run 'fx build host_x64/fidl-lint' to rebuild it? [Y/n] " >/dev/tty | |
| read | |
| if [[ $REPLY == "" || $REPLY =~ Y|y ]]; then | |
| fx-command-run build "${FIDL_LINT_TARGET}" >/dev/tty 2>&1 || return $? | |
| fi | |
| fi | |
| break | |
| fi | |
| done | |
| fi | |
| return 0 | |
| } | |
| execute() { | |
| if [[ -n "${VERBOSE}" || -n "${DRY_RUN}" ]]; then | |
| echo "$@" | |
| echo | |
| fi | |
| if [[ ! -n "${DRY_RUN}" ]]; then | |
| "$@" || return $? | |
| fi | |
| } | |
| call_linters() { | |
| if (( "${#CLANG_TIDY_CMD[@]}" > 0 && "${#CLANG_TIDY_FILES[@]}" > 0 )); then | |
| execute "${CLANG_TIDY_CMD[@]}" "${COMMAND_ARGS[@]}" "${CLANG_TIDY_FILES[@]}" || return $? | |
| fi | |
| if (( "${#DART_LINT_CMD[@]}" > 0 && "${#DART_LINT_FILES[@]}" > 0 )); then | |
| execute "${DART_LINT_CMD[@]}" "${COMMAND_ARGS[@]}" "${DART_LINT_FILES[@]}" || return $? | |
| fi | |
| if (( "${#FIDL_LINT_CMD[@]}" > 0 && "${#FIDL_LINT_FILES[@]}" > 0 )); then | |
| execute ensure_fidl_lint || return $? | |
| execute "${FIDL_LINT_CMD[@]}" "${COMMAND_ARGS[@]}" "${FIDL_LINT_FILES[@]}" || return $? | |
| fi | |
| if (( "${#GO_LINT_CMD[@]}" > 0 && "${#GO_LINT_FILES[@]}" > 0 )); then | |
| execute "${GO_LINT_CMD[@]}" "${COMMAND_ARGS[@]}" "${GO_LINT_FILES[@]}" || return $? | |
| fi | |
| if (( "${#JSON_LINT_CMD[@]}" > 0 && "${#JSON_LINT_FILES[@]}" > 0 )); then | |
| execute "${JSON_LINT_CMD[@]}" "${COMMAND_ARGS[@]}" "${JSON_LINT_FILES[@]}" || return $? | |
| fi | |
| if [[ "${#RUST_LINT_CMD[@]}" > 0 && (( "${#RUST_LINT_FILES[@]}" > 0 || -n "${TARGET}" )) ]]; then | |
| local args=() | |
| if [[ -n "${TARGET}" ]]; then | |
| args+=( "--target=${TARGET}" ) | |
| fi | |
| for file in ${RUST_LINT_FILES}; do | |
| args+=( "--file=${file}" ) | |
| done | |
| execute "${RUST_LINT_CMD[@]}" "${COMMAND_ARGS[@]}" "${args}" || return $? | |
| fi | |
| } | |
| for file in ${FILES}; do | |
| add_file_to_linter ${file} | |
| done | |
| call_linters || exit $? |