You are here

commit-code-check.sh in Drupal 10

#!/bin/bash
#
# This script performs code quality checks.
#
# @internal
#   This script is not covered by Drupal core's backwards compatibility promise.
#   It exists only for core development purposes.
#
# The script makes the following checks:
# - Spell checking.
# - File modes.
# - No changes to core/node_modules directory.
# - PHPCS checks PHP and YAML files.
# - PHPStan checks PHP files.
# - ESLint checks JavaScript and YAML files.
# - Checks .es6.js and .js files are equivalent.
# - Stylelint checks CSS files.
# - Checks .pcss.css and .css files are equivalent.

# cSpell:disable

# Searches an array.
contains_element() {
  local e
  for e in ${@:2}; do [[ "$e" == "$1" ]] && return 0; done
  return 1
}

CACHED=0
DRUPALCI=0
BRANCH=""
while test $# -gt 0; do
  case "$1" in
    -h|--help)
      echo "Drupal code quality checks"
      echo " "
      echo "options:"
      echo "-h, --help                show brief help"
      echo "--branch BRANCH           creates list of files to check by comparing against a branch"
      echo "--cached                  checks staged files"
      echo "--drupalci                a special mode for DrupalCI"
      echo " "
      echo "Example usage: sh ./core/scripts/dev/commit-code-check.sh --branch 9.2.x"
      exit 0
      ;;
    --branch)
      BRANCH="$2"
      if [[ "$BRANCH" == "" ]]; then
        printf "The --branch option requires a value. For example: --branch 9.2.x\n"
        exit;
      fi
      shift 2
      ;;
    --cached)
      CACHED=1
      shift
      ;;
    --drupalci)
      DRUPALCI=1
      shift
      ;;
    *)
      break
      ;;
  esac
done

# Set up variables to make colored output simple. Color output is disabled on
# DrupalCI because it is breaks reporting.
# @todo https://www.drupal.org/project/drupalci_testbot/issues/3181869
if [[ "$DRUPALCI" == "1" ]]; then
  red=""
  green=""
  reset=""
  DRUPAL_VERSION=$(php -r "include 'vendor/autoload.php'; print preg_replace('#\.[0-9]+-dev#', '.x', \Drupal::VERSION);")
else
  red=$(tput setaf 1 && tput bold)
  green=$(tput setaf 2)
  reset=$(tput sgr0)
fi

# Gets list of files to check.
if [[ "$BRANCH" != "" ]]; then
  FILES=$(git diff --name-only $BRANCH HEAD);
elif [[ "$CACHED" == "0" ]]; then
  # For DrupalCI patch testing or when running without --cached or --branch,
  # list of all changes in the working directory.
  FILES=$(git ls-files --other --modified --exclude-standard --exclude=vendor)
else
  # Check staged files only.
  if git rev-parse --verify HEAD >/dev/null 2>&1
  then
    AGAINST=HEAD
  else
    # Initial commit: diff against an empty tree object
    AGAINST=4b825dc642cb6eb9a060e54bf8d69288fbee4904
  fi
  FILES=$(git diff --cached --name-only $AGAINST);
fi

if [[ "$FILES" == "" ]] && [[ "$DRUPALCI" == "1" ]]; then
  # If the FILES is empty we might be testing a merge request on DrupalCI. We
  # need to diff against the Drupal branch or tag related to the Drupal version.
  printf "Creating list of files to check by comparing branch to %s\n" "$DRUPAL_VERSION"
  # On DrupalCI there's a merge commit so we can compare to HEAD~1.
  FILES=$(git diff --name-only HEAD~1 HEAD);
fi

TOP_LEVEL=$(git rev-parse --show-toplevel)

# This variable will be set to one when the file core/phpcs.xml.dist is changed.
PHPCS_XML_DIST_FILE_CHANGED=0

# This variable will be set to one when the files core/phpstan-baseline.neon or
# core/phpstan.neon.dist are changed.
PHPSTAN_DIST_FILE_CHANGED=0

# This variable will be set to one when one of the eslint config file is
# changed:
#  - core/.eslintrc.passing.json
#  - core/.eslintrc.json
#  - core/.eslintrc.jquery.json
ESLINT_CONFIG_PASSING_FILE_CHANGED=0

# This variable will be set to one when the stylelint config file is changed.
# changed:
#  - core/.stylelintignore
#  - core/.stylelintrc.json
STYLELINT_CONFIG_FILE_CHANGED=0

# This variable will be set when a Drupal-specific CKEditor 5 plugin has changed
# it is used to make sure the compiled JS is valid.
CKEDITOR5_PLUGINS_CHANGED=0

# Build up a list of absolute file names.
ABS_FILES=
for FILE in $FILES; do
  if [ -f "$TOP_LEVEL/$FILE" ]; then
    ABS_FILES="$ABS_FILES $TOP_LEVEL/$FILE"
  fi

  if [[ $FILE == "core/phpcs.xml.dist" ]]; then
    PHPCS_XML_DIST_FILE_CHANGED=1;
  fi;

  if [[ $FILE == "core/phpstan-baseline.neon" || $FILE == "core/phpstan.neon.dist" ]]; then
    PHPSTAN_DIST_FILE_CHANGED=1;
  fi;

  if [[ $FILE == "core/.eslintrc.json" || $FILE == "core/.eslintrc.passing.json" || $FILE == "core/.eslintrc.jquery.json" ]]; then
    ESLINT_CONFIG_PASSING_FILE_CHANGED=1;
  fi;

  if [[ $FILE == "core/.stylelintignore" || $FILE == "core/.stylelintrc.json" ]]; then
    STYLELINT_CONFIG_FILE_CHANGED=1;
  fi;

  # If JavaScript packages change, then rerun all JavaScript style checks.
  if [[ $FILE == "core/package.json" || $FILE == "core/yarn.lock" ]]; then
    ESLINT_CONFIG_PASSING_FILE_CHANGED=1;
    STYLELINT_CONFIG_FILE_CHANGED=1;
  fi;

  if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.js$ ]] && [[ $FILE =~ ^core/modules/ckeditor5/js/build || $FILE =~ ^core/modules/ckeditor5/js/ckeditor5_plugins ]]; then
    CKEDITOR5_PLUGINS_CHANGED=1;
  fi;
done

# Exit early if there are no files.
if [[ "$ABS_FILES" == "" ]]; then
  printf "There are no files to check. If you have staged a commit use the --cached option.\n"
  exit;
fi;

# This script assumes that composer install and yarn install have already been
# run and all dependencies are updated.
FINAL_STATUS=0

DEPENDENCIES_NEED_INSTALLING=0
# Ensure PHP development dependencies are installed.
# @todo https://github.com/composer/composer/issues/4497 Improve this to
#  determine if dependencies in the lock file match the installed versions.
#  Using composer install --dry-run is not valid because it would depend on
#  user-facing strings in Composer.
if ! [[ -f 'vendor/bin/phpcs' ]]; then
  printf "Drupal's PHP development dependencies are not installed. Run 'composer install' from the root directory.\n"
  DEPENDENCIES_NEED_INSTALLING=1;
fi

cd "$TOP_LEVEL/core"

# Ensure JavaScript development dependencies are installed.
yarn check -s 2>/dev/null
if [ "$?" -ne "0" ]; then
  printf "Drupal's JavaScript development dependencies are not installed or cannot be resolved. Run 'yarn install' inside the core directory, or 'yarn check -s' to list other errors.\n"
  DEPENDENCIES_NEED_INSTALLING=1;
fi

if [ $DEPENDENCIES_NEED_INSTALLING -ne 0 ]; then
  exit 1;
fi

# Check all files for spelling in one go for better performance.
yarn run -s spellcheck --no-must-find-files -c $TOP_LEVEL/core/.cspell.json $ABS_FILES
if [ "$?" -ne "0" ]; then
  # If there are failures set the status to a number other than 0.
  FINAL_STATUS=1
  printf "\nCSpell: ${red}failed${reset}\n"
else
  printf "\nCSpell: ${green}passed${reset}\n"
fi
cd "$TOP_LEVEL"

# Add a separator line to make the output easier to read.
printf "\n"
printf -- '-%.0s' {1..100}
printf "\n"

# Run PHPStan on all files on DrupalCI or when phpstan files are changed.
# APCu is disabled to ensure that the composer classmap is not corrupted.
if [[ $PHPSTAN_DIST_FILE_CHANGED == "1" ]] || [[ "$DRUPALCI" == "1" ]]; then
  printf "\nRunning PHPStan on *all* files.\n"
  php -d apc.enabled=0 -d apc.enable_cli=0 vendor/bin/phpstan analyze --no-progress --configuration="$TOP_LEVEL/core/phpstan.neon.dist"
else
  # Only run PHPStan on changed files locally.
  printf "\nRunning PHPStan on changed files.\n"
  php -d apc.enabled=0 -d apc.enable_cli=0 vendor/bin/phpstan analyze --no-progress --configuration="$TOP_LEVEL/core/phpstan-partial.neon" $ABS_FILES
fi

if [ "$?" -ne "0" ]; then
  # If there are failures set the status to a number other than 0.
  FINAL_STATUS=1
  printf "\nPHPStan: ${red}failed${reset}\n"
else
  printf "\nPHPStan: ${green}passed${reset}\n"
fi

# Add a separator line to make the output easier to read.
printf "\n"
printf -- '-%.0s' {1..100}
printf "\n"

# When the file core/phpcs.xml.dist has been changed, then PHPCS must check all files.
if [[ $PHPCS_XML_DIST_FILE_CHANGED == "1" ]]; then
  # Test all files with phpcs rules.
  vendor/bin/phpcs -ps --standard="$TOP_LEVEL/core/phpcs.xml.dist"
  PHPCS=$?
  if [ "$PHPCS" -ne "0" ]; then
    # If there are failures set the status to a number other than 0.
    FINAL_STATUS=1
    printf "\nPHPCS: ${red}failed${reset}\n"
  else
    printf "\nPHPCS: ${green}passed${reset}\n"
  fi
  # Add a separator line to make the output easier to read.
  printf "\n"
  printf -- '-%.0s' {1..100}
  printf "\n"
fi

# When the eslint config has been changed, then eslint must check all files.
if [[ $ESLINT_CONFIG_PASSING_FILE_CHANGED == "1" ]]; then
  cd "$TOP_LEVEL/core"
  yarn run -s lint:core-js-passing "$TOP_LEVEL/core"
  CORRECTJS=$?
  if [ "$CORRECTJS" -ne "0" ]; then
    # If there are failures set the status to a number other than 0.
    FINAL_STATUS=1
    printf "\neslint: ${red}failed${reset}\n"
  else
    printf "\neslint: ${green}passed${reset}\n"
  fi
  cd $TOP_LEVEL
  # Add a separator line to make the output easier to read.
  printf "\n"
  printf -- '-%.0s' {1..100}
  printf "\n"
fi

# When the stylelint config has been changed, then stylelint must check all files.
if [[ $STYLELINT_CONFIG_FILE_CHANGED == "1" ]]; then
  cd "$TOP_LEVEL/core"
  yarn run -s lint:css
  if [ "$?" -ne "0" ]; then
    # If there are failures set the status to a number other than 0.
    FINAL_STATUS=1
    printf "\nstylelint: ${red}failed${reset}\n"
  else
    printf "\nstylelint: ${green}passed${reset}\n"
  fi
  cd $TOP_LEVEL
  # Add a separator line to make the output easier to read.
  printf "\n"
  printf -- '-%.0s' {1..100}
  printf "\n"
fi

# When a Drupal-specific CKEditor 5 plugin changed ensure that it is compiled
# properly. Only check on DrupalCI, since we're concerned about the build being
# run with the expected package versions and making sure the result of the build
# is in sync and conform to expectations.
if [[ "$DRUPALCI" == "1" ]] && [[ $CKEDITOR5_PLUGINS_CHANGED == "1" ]]; then
  cd "$TOP_LEVEL/core"
  yarn run -s check:ckeditor5
  if [ "$?" -ne "0" ]; then
    # If there are failures set the status to a number other than 0.
    FINAL_STATUS=1
    printf "\nDrupal-specific CKEditor 5 plugins: ${red}failed${reset}\n"
  else
    printf "\nDrupal-specific CKEditor 5 plugins: ${green}passed${reset}\n"
  fi
  cd $TOP_LEVEL
  # Add a separator line to make the output easier to read.
  printf "\n"
  printf -- '-%.0s' {1..100}
  printf "\n"
fi

for FILE in $FILES; do
  STATUS=0;
  # Print a line to separate spellcheck output from per file output.
  printf "Checking %s\n" "$FILE"
  printf "\n"

  # Ensure the file still exists (i.e. is not being deleted).
  if [ -a $FILE ]; then
    if [ ${FILE: -3} != ".sh" ]; then
      # Ensure the file has the correct mode.
      STAT="$(stat -f "%A" $FILE 2>/dev/null)"
      if [ $? -ne 0 ]; then
        STAT="$(stat -c "%a" $FILE 2>/dev/null)"
      fi
      if [ "$STAT" -ne "644" ]; then
        printf "${red}check failed:${reset} file $FILE should be 644 not $STAT\n"
        STATUS=1
      fi
    fi
  fi

  # Don't commit changes to vendor.
  if [[ "$FILE" =~ ^vendor/ ]]; then
    printf "${red}check failed:${reset} file in vendor directory being committed ($FILE)\n"
    STATUS=1
  fi

  # Don't commit changes to core/node_modules.
  if [[ "$FILE" =~ ^core/node_modules/ ]]; then
    printf "${red}check failed:${reset} file in core/node_modules directory being committed ($FILE)\n"
    STATUS=1
  fi

  ############################################################################
  ### PHP AND YAML FILES
  ############################################################################
  if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.(inc|install|module|php|profile|test|theme|yml)$ ]] && [[ $PHPCS_XML_DIST_FILE_CHANGED == "0" ]]; then
    # Test files with phpcs rules.
    vendor/bin/phpcs "$TOP_LEVEL/$FILE" --standard="$TOP_LEVEL/core/phpcs.xml.dist"
    PHPCS=$?
    if [ "$PHPCS" -ne "0" ]; then
      # If there are failures set the status to a number other than 0.
      STATUS=1
    else
      printf "PHPCS: $FILE ${green}passed${reset}\n"
    fi
  fi

  ############################################################################
  ### YAML FILES
  ############################################################################
  if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.yml$ ]]; then
    # Test files with ESLint.
    cd "$TOP_LEVEL/core"
    node ./node_modules/eslint/bin/eslint.js --quiet --resolve-plugins-relative-to . "$TOP_LEVEL/$FILE"
    YAMLLINT=$?
    if [ "$YAMLLINT" -ne "0" ]; then
      # If there are failures set the status to a number other than 0.
      STATUS=1
    else
      printf "ESLint: $FILE ${green}passed${reset}\n"
    fi
    cd $TOP_LEVEL
  fi

  ############################################################################
  ### JAVASCRIPT FILES
  ############################################################################
  if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.js$ ]] && [[ ! $FILE =~ ^core/tests/Drupal/Nightwatch ]] && [[ ! $FILE =~ /tests/src/Nightwatch/ ]] && [[ ! $FILE =~ ^core/modules/ckeditor5/js/ckeditor5_plugins ]]; then
    # Work out the root name of the JavaScript so we can ensure that the ES6
    # version has been compiled correctly.
    if [[ $FILE =~ \.es6\.js$ ]]; then
      BASENAME=${FILE%.es6.js}
      COMPILE_CHECK=1
    else
      BASENAME=${FILE%.js}
      # We only need to compile check if the .es6.js file is not also
      # changing. This is because the compile check will occur for the
      # .es6.js file. This might occur if the compile scripts have changed.
      contains_element "$BASENAME.es6.js" "${FILES[@]}"
      HASES6=$?
      if [ "$HASES6" -ne "0" ]; then
        COMPILE_CHECK=1
      else
        COMPILE_CHECK=0
      fi
    fi
    if [[ "$COMPILE_CHECK" == "1" ]] && [[ -f "$TOP_LEVEL/$BASENAME.es6.js" ]]; then
      cd "$TOP_LEVEL/core"
      yarn run build:js --check --file "$TOP_LEVEL/$BASENAME.es6.js"
      CORRECTJS=$?
      if [ "$CORRECTJS" -ne "0" ]; then
        # No need to write any output the yarn run command will do this for
        # us.
        STATUS=1
      fi
      # Check the coding standards.
      if [[ -f ".eslintrc.passing.json" ]]; then
        node ./node_modules/eslint/bin/eslint.js --quiet --config=.eslintrc.passing.json "$TOP_LEVEL/$BASENAME.es6.js"
        CORRECTJS=$?
        if [ "$CORRECTJS" -ne "0" ]; then
          # No need to write any output the node command will do this for us.
          STATUS=1
        fi
      fi
      cd $TOP_LEVEL
    else
      # If there is no .es6.js file then there should be unless the .js is
      # not really Drupal's.
      if ! [[ "$FILE" =~ ^core/assets/vendor ]] && ! [[ "$FILE" =~ ^core/modules/ckeditor5/js/build ]] && ! [[ "$FILE" =~ ^core/scripts/js ]] && ! [[ "$FILE" =~ ^core/scripts/css ]] && ! [[ "$FILE" =~ webpack.config.js$ ]] && ! [[ -f "$TOP_LEVEL/$BASENAME.es6.js" ]] && ! [[ "$FILE" =~ core/modules/ckeditor5/tests/modules/ckeditor5_test/js/build/layercake.js ]]; then
        printf "${red}FAILURE${reset} $FILE does not have a corresponding $BASENAME.es6.js\n"
        STATUS=1
      fi
    fi
  else
    # Check coding standards of Nightwatch files.
    if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.js$ ]]; then
      cd "$TOP_LEVEL/core"
      # Check the coding standards.
      if [[ -f ".eslintrc.passing.json" ]]; then
        node ./node_modules/eslint/bin/eslint.js --quiet --config=.eslintrc.passing.json "$TOP_LEVEL/$FILE"
        CORRECTJS=$?
        if [ "$CORRECTJS" -ne "0" ]; then
          # No need to write any output the node command will do this for us.
          STATUS=1
        fi
      fi
      cd $TOP_LEVEL
    fi
  fi

  ############################################################################
  ### CSS FILES
  ############################################################################
  if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.css$ ]]; then
    # Work out the root name of the CSS so we can ensure that the PostCSS
    # version has been compiled correctly.
    if [[ $FILE =~ \.pcss\.css$ ]]; then
      BASENAME=${FILE%.pcss.css}
      COMPILE_CHECK=1
    else
      BASENAME=${FILE%.css}
      # We only need to compile check if the .pcss.css file is not also
      # changing. This is because the compile check will occur for the
      # .pcss.css file. This might occur if the compiled stylesheets have
      # changed.
      contains_element "$BASENAME.pcss.css" "${FILES[@]}"
      HASPOSTCSS=$?
      if [ "$HASPOSTCSS" -ne "0" ]; then
        COMPILE_CHECK=1
      else
        COMPILE_CHECK=0
      fi
    fi
    # PostCSS
    if [[ "$COMPILE_CHECK" == "1" ]] && [[ -f "$TOP_LEVEL/$BASENAME.pcss.css" ]]; then
      cd "$TOP_LEVEL/core"
      yarn run build:css --check --file "$TOP_LEVEL/$BASENAME.pcss.css"
      CORRECTCSS=$?
      if [ "$CORRECTCSS" -ne "0" ]; then
        # No need to write any output the yarn run command will do this for
        # us.
        STATUS=1
      fi
      cd $TOP_LEVEL
    fi
  fi
  if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.css$ ]] && [[ -f "core/node_modules/.bin/stylelint" ]]; then
    BASENAME=${FILE%.css}
    # We only need to use stylelint on the .pcss.css file. So if this CSS file
    # has a corresponding .pcss don't do stylelint.
    if [[ $FILE =~ \.pcss\.css$ ]] || [[ ! -f "$TOP_LEVEL/$BASENAME.pcss.css" ]]; then
      cd "$TOP_LEVEL/core"
      node_modules/.bin/stylelint --allow-empty-input "$TOP_LEVEL/$FILE"
      if [ "$?" -ne "0" ]; then
        STATUS=1
      else
        printf "STYLELINT: $FILE ${green}passed${reset}\n"
      fi
      cd $TOP_LEVEL
    fi
  fi

  if [[ "$STATUS" == "1" ]]; then
    FINAL_STATUS=1
    # There is no need to print a failure message. The fail will be described
    # already.
  else
    printf "%s ${green}passed${reset}\n" "$FILE"
  fi

  # Print a line to separate each file's checks.
  printf "\n"
  printf -- '-%.0s' {1..100}
  printf "\n"
done

if [[ "$FINAL_STATUS" == "1" ]] && [[ "$DRUPALCI" == "1" ]]; then
  printf "${red}Drupal code quality checks failed.${reset}\n"
  printf "To reproduce this output locally:\n"
  printf "* Apply the change as a patch\n"
  printf "* Run this command locally: sh ./core/scripts/dev/commit-code-check.sh\n"
  printf "OR:\n"
  printf "* From the merge request branch\n"
  printf "* Run this command locally: sh ./core/scripts/dev/commit-code-check.sh --branch %s\n" "$DRUPAL_VERSION"
fi
exit $FINAL_STATUS

File

core/scripts/dev/commit-code-check.sh
View source
  1. #!/bin/bash
  2. #
  3. # This script performs code quality checks.
  4. #
  5. # @internal
  6. # This script is not covered by Drupal core's backwards compatibility promise.
  7. # It exists only for core development purposes.
  8. #
  9. # The script makes the following checks:
  10. # - Spell checking.
  11. # - File modes.
  12. # - No changes to core/node_modules directory.
  13. # - PHPCS checks PHP and YAML files.
  14. # - PHPStan checks PHP files.
  15. # - ESLint checks JavaScript and YAML files.
  16. # - Checks .es6.js and .js files are equivalent.
  17. # - Stylelint checks CSS files.
  18. # - Checks .pcss.css and .css files are equivalent.
  19. # cSpell:disable
  20. # Searches an array.
  21. contains_element() {
  22. local e
  23. for e in ${@:2}; do [[ "$e" == "$1" ]] && return 0; done
  24. return 1
  25. }
  26. CACHED=0
  27. DRUPALCI=0
  28. BRANCH=""
  29. while test $# -gt 0; do
  30. case "$1" in
  31. -h|--help)
  32. echo "Drupal code quality checks"
  33. echo " "
  34. echo "options:"
  35. echo "-h, --help show brief help"
  36. echo "--branch BRANCH creates list of files to check by comparing against a branch"
  37. echo "--cached checks staged files"
  38. echo "--drupalci a special mode for DrupalCI"
  39. echo " "
  40. echo "Example usage: sh ./core/scripts/dev/commit-code-check.sh --branch 9.2.x"
  41. exit 0
  42. ;;
  43. --branch)
  44. BRANCH="$2"
  45. if [[ "$BRANCH" == "" ]]; then
  46. printf "The --branch option requires a value. For example: --branch 9.2.x\n"
  47. exit;
  48. fi
  49. shift 2
  50. ;;
  51. --cached)
  52. CACHED=1
  53. shift
  54. ;;
  55. --drupalci)
  56. DRUPALCI=1
  57. shift
  58. ;;
  59. *)
  60. break
  61. ;;
  62. esac
  63. done
  64. # Set up variables to make colored output simple. Color output is disabled on
  65. # DrupalCI because it is breaks reporting.
  66. # @todo https://www.drupal.org/project/drupalci_testbot/issues/3181869
  67. if [[ "$DRUPALCI" == "1" ]]; then
  68. red=""
  69. green=""
  70. reset=""
  71. DRUPAL_VERSION=$(php -r "include 'vendor/autoload.php'; print preg_replace('#\.[0-9]+-dev#', '.x', \Drupal::VERSION);")
  72. else
  73. red=$(tput setaf 1 && tput bold)
  74. green=$(tput setaf 2)
  75. reset=$(tput sgr0)
  76. fi
  77. # Gets list of files to check.
  78. if [[ "$BRANCH" != "" ]]; then
  79. FILES=$(git diff --name-only $BRANCH HEAD);
  80. elif [[ "$CACHED" == "0" ]]; then
  81. # For DrupalCI patch testing or when running without --cached or --branch,
  82. # list of all changes in the working directory.
  83. FILES=$(git ls-files --other --modified --exclude-standard --exclude=vendor)
  84. else
  85. # Check staged files only.
  86. if git rev-parse --verify HEAD >/dev/null 2>&1
  87. then
  88. AGAINST=HEAD
  89. else
  90. # Initial commit: diff against an empty tree object
  91. AGAINST=4b825dc642cb6eb9a060e54bf8d69288fbee4904
  92. fi
  93. FILES=$(git diff --cached --name-only $AGAINST);
  94. fi
  95. if [[ "$FILES" == "" ]] && [[ "$DRUPALCI" == "1" ]]; then
  96. # If the FILES is empty we might be testing a merge request on DrupalCI. We
  97. # need to diff against the Drupal branch or tag related to the Drupal version.
  98. printf "Creating list of files to check by comparing branch to %s\n" "$DRUPAL_VERSION"
  99. # On DrupalCI there's a merge commit so we can compare to HEAD~1.
  100. FILES=$(git diff --name-only HEAD~1 HEAD);
  101. fi
  102. TOP_LEVEL=$(git rev-parse --show-toplevel)
  103. # This variable will be set to one when the file core/phpcs.xml.dist is changed.
  104. PHPCS_XML_DIST_FILE_CHANGED=0
  105. # This variable will be set to one when the files core/phpstan-baseline.neon or
  106. # core/phpstan.neon.dist are changed.
  107. PHPSTAN_DIST_FILE_CHANGED=0
  108. # This variable will be set to one when one of the eslint config file is
  109. # changed:
  110. # - core/.eslintrc.passing.json
  111. # - core/.eslintrc.json
  112. # - core/.eslintrc.jquery.json
  113. ESLINT_CONFIG_PASSING_FILE_CHANGED=0
  114. # This variable will be set to one when the stylelint config file is changed.
  115. # changed:
  116. # - core/.stylelintignore
  117. # - core/.stylelintrc.json
  118. STYLELINT_CONFIG_FILE_CHANGED=0
  119. # This variable will be set when a Drupal-specific CKEditor 5 plugin has changed
  120. # it is used to make sure the compiled JS is valid.
  121. CKEDITOR5_PLUGINS_CHANGED=0
  122. # Build up a list of absolute file names.
  123. ABS_FILES=
  124. for FILE in $FILES; do
  125. if [ -f "$TOP_LEVEL/$FILE" ]; then
  126. ABS_FILES="$ABS_FILES $TOP_LEVEL/$FILE"
  127. fi
  128. if [[ $FILE == "core/phpcs.xml.dist" ]]; then
  129. PHPCS_XML_DIST_FILE_CHANGED=1;
  130. fi;
  131. if [[ $FILE == "core/phpstan-baseline.neon" || $FILE == "core/phpstan.neon.dist" ]]; then
  132. PHPSTAN_DIST_FILE_CHANGED=1;
  133. fi;
  134. if [[ $FILE == "core/.eslintrc.json" || $FILE == "core/.eslintrc.passing.json" || $FILE == "core/.eslintrc.jquery.json" ]]; then
  135. ESLINT_CONFIG_PASSING_FILE_CHANGED=1;
  136. fi;
  137. if [[ $FILE == "core/.stylelintignore" || $FILE == "core/.stylelintrc.json" ]]; then
  138. STYLELINT_CONFIG_FILE_CHANGED=1;
  139. fi;
  140. # If JavaScript packages change, then rerun all JavaScript style checks.
  141. if [[ $FILE == "core/package.json" || $FILE == "core/yarn.lock" ]]; then
  142. ESLINT_CONFIG_PASSING_FILE_CHANGED=1;
  143. STYLELINT_CONFIG_FILE_CHANGED=1;
  144. fi;
  145. if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.js$ ]] && [[ $FILE =~ ^core/modules/ckeditor5/js/build || $FILE =~ ^core/modules/ckeditor5/js/ckeditor5_plugins ]]; then
  146. CKEDITOR5_PLUGINS_CHANGED=1;
  147. fi;
  148. done
  149. # Exit early if there are no files.
  150. if [[ "$ABS_FILES" == "" ]]; then
  151. printf "There are no files to check. If you have staged a commit use the --cached option.\n"
  152. exit;
  153. fi;
  154. # This script assumes that composer install and yarn install have already been
  155. # run and all dependencies are updated.
  156. FINAL_STATUS=0
  157. DEPENDENCIES_NEED_INSTALLING=0
  158. # Ensure PHP development dependencies are installed.
  159. # @todo https://github.com/composer/composer/issues/4497 Improve this to
  160. # determine if dependencies in the lock file match the installed versions.
  161. # Using composer install --dry-run is not valid because it would depend on
  162. # user-facing strings in Composer.
  163. if ! [[ -f 'vendor/bin/phpcs' ]]; then
  164. printf "Drupal's PHP development dependencies are not installed. Run 'composer install' from the root directory.\n"
  165. DEPENDENCIES_NEED_INSTALLING=1;
  166. fi
  167. cd "$TOP_LEVEL/core"
  168. # Ensure JavaScript development dependencies are installed.
  169. yarn check -s 2>/dev/null
  170. if [ "$?" -ne "0" ]; then
  171. printf "Drupal's JavaScript development dependencies are not installed or cannot be resolved. Run 'yarn install' inside the core directory, or 'yarn check -s' to list other errors.\n"
  172. DEPENDENCIES_NEED_INSTALLING=1;
  173. fi
  174. if [ $DEPENDENCIES_NEED_INSTALLING -ne 0 ]; then
  175. exit 1;
  176. fi
  177. # Check all files for spelling in one go for better performance.
  178. yarn run -s spellcheck --no-must-find-files -c $TOP_LEVEL/core/.cspell.json $ABS_FILES
  179. if [ "$?" -ne "0" ]; then
  180. # If there are failures set the status to a number other than 0.
  181. FINAL_STATUS=1
  182. printf "\nCSpell: ${red}failed${reset}\n"
  183. else
  184. printf "\nCSpell: ${green}passed${reset}\n"
  185. fi
  186. cd "$TOP_LEVEL"
  187. # Add a separator line to make the output easier to read.
  188. printf "\n"
  189. printf -- '-%.0s' {1..100}
  190. printf "\n"
  191. # Run PHPStan on all files on DrupalCI or when phpstan files are changed.
  192. # APCu is disabled to ensure that the composer classmap is not corrupted.
  193. if [[ $PHPSTAN_DIST_FILE_CHANGED == "1" ]] || [[ "$DRUPALCI" == "1" ]]; then
  194. printf "\nRunning PHPStan on *all* files.\n"
  195. php -d apc.enabled=0 -d apc.enable_cli=0 vendor/bin/phpstan analyze --no-progress --configuration="$TOP_LEVEL/core/phpstan.neon.dist"
  196. else
  197. # Only run PHPStan on changed files locally.
  198. printf "\nRunning PHPStan on changed files.\n"
  199. php -d apc.enabled=0 -d apc.enable_cli=0 vendor/bin/phpstan analyze --no-progress --configuration="$TOP_LEVEL/core/phpstan-partial.neon" $ABS_FILES
  200. fi
  201. if [ "$?" -ne "0" ]; then
  202. # If there are failures set the status to a number other than 0.
  203. FINAL_STATUS=1
  204. printf "\nPHPStan: ${red}failed${reset}\n"
  205. else
  206. printf "\nPHPStan: ${green}passed${reset}\n"
  207. fi
  208. # Add a separator line to make the output easier to read.
  209. printf "\n"
  210. printf -- '-%.0s' {1..100}
  211. printf "\n"
  212. # When the file core/phpcs.xml.dist has been changed, then PHPCS must check all files.
  213. if [[ $PHPCS_XML_DIST_FILE_CHANGED == "1" ]]; then
  214. # Test all files with phpcs rules.
  215. vendor/bin/phpcs -ps --standard="$TOP_LEVEL/core/phpcs.xml.dist"
  216. PHPCS=$?
  217. if [ "$PHPCS" -ne "0" ]; then
  218. # If there are failures set the status to a number other than 0.
  219. FINAL_STATUS=1
  220. printf "\nPHPCS: ${red}failed${reset}\n"
  221. else
  222. printf "\nPHPCS: ${green}passed${reset}\n"
  223. fi
  224. # Add a separator line to make the output easier to read.
  225. printf "\n"
  226. printf -- '-%.0s' {1..100}
  227. printf "\n"
  228. fi
  229. # When the eslint config has been changed, then eslint must check all files.
  230. if [[ $ESLINT_CONFIG_PASSING_FILE_CHANGED == "1" ]]; then
  231. cd "$TOP_LEVEL/core"
  232. yarn run -s lint:core-js-passing "$TOP_LEVEL/core"
  233. CORRECTJS=$?
  234. if [ "$CORRECTJS" -ne "0" ]; then
  235. # If there are failures set the status to a number other than 0.
  236. FINAL_STATUS=1
  237. printf "\neslint: ${red}failed${reset}\n"
  238. else
  239. printf "\neslint: ${green}passed${reset}\n"
  240. fi
  241. cd $TOP_LEVEL
  242. # Add a separator line to make the output easier to read.
  243. printf "\n"
  244. printf -- '-%.0s' {1..100}
  245. printf "\n"
  246. fi
  247. # When the stylelint config has been changed, then stylelint must check all files.
  248. if [[ $STYLELINT_CONFIG_FILE_CHANGED == "1" ]]; then
  249. cd "$TOP_LEVEL/core"
  250. yarn run -s lint:css
  251. if [ "$?" -ne "0" ]; then
  252. # If there are failures set the status to a number other than 0.
  253. FINAL_STATUS=1
  254. printf "\nstylelint: ${red}failed${reset}\n"
  255. else
  256. printf "\nstylelint: ${green}passed${reset}\n"
  257. fi
  258. cd $TOP_LEVEL
  259. # Add a separator line to make the output easier to read.
  260. printf "\n"
  261. printf -- '-%.0s' {1..100}
  262. printf "\n"
  263. fi
  264. # When a Drupal-specific CKEditor 5 plugin changed ensure that it is compiled
  265. # properly. Only check on DrupalCI, since we're concerned about the build being
  266. # run with the expected package versions and making sure the result of the build
  267. # is in sync and conform to expectations.
  268. if [[ "$DRUPALCI" == "1" ]] && [[ $CKEDITOR5_PLUGINS_CHANGED == "1" ]]; then
  269. cd "$TOP_LEVEL/core"
  270. yarn run -s check:ckeditor5
  271. if [ "$?" -ne "0" ]; then
  272. # If there are failures set the status to a number other than 0.
  273. FINAL_STATUS=1
  274. printf "\nDrupal-specific CKEditor 5 plugins: ${red}failed${reset}\n"
  275. else
  276. printf "\nDrupal-specific CKEditor 5 plugins: ${green}passed${reset}\n"
  277. fi
  278. cd $TOP_LEVEL
  279. # Add a separator line to make the output easier to read.
  280. printf "\n"
  281. printf -- '-%.0s' {1..100}
  282. printf "\n"
  283. fi
  284. for FILE in $FILES; do
  285. STATUS=0;
  286. # Print a line to separate spellcheck output from per file output.
  287. printf "Checking %s\n" "$FILE"
  288. printf "\n"
  289. # Ensure the file still exists (i.e. is not being deleted).
  290. if [ -a $FILE ]; then
  291. if [ ${FILE: -3} != ".sh" ]; then
  292. # Ensure the file has the correct mode.
  293. STAT="$(stat -f "%A" $FILE 2>/dev/null)"
  294. if [ $? -ne 0 ]; then
  295. STAT="$(stat -c "%a" $FILE 2>/dev/null)"
  296. fi
  297. if [ "$STAT" -ne "644" ]; then
  298. printf "${red}check failed:${reset} file $FILE should be 644 not $STAT\n"
  299. STATUS=1
  300. fi
  301. fi
  302. fi
  303. # Don't commit changes to vendor.
  304. if [[ "$FILE" =~ ^vendor/ ]]; then
  305. printf "${red}check failed:${reset} file in vendor directory being committed ($FILE)\n"
  306. STATUS=1
  307. fi
  308. # Don't commit changes to core/node_modules.
  309. if [[ "$FILE" =~ ^core/node_modules/ ]]; then
  310. printf "${red}check failed:${reset} file in core/node_modules directory being committed ($FILE)\n"
  311. STATUS=1
  312. fi
  313. ############################################################################
  314. ### PHP AND YAML FILES
  315. ############################################################################
  316. if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.(inc|install|module|php|profile|test|theme|yml)$ ]] && [[ $PHPCS_XML_DIST_FILE_CHANGED == "0" ]]; then
  317. # Test files with phpcs rules.
  318. vendor/bin/phpcs "$TOP_LEVEL/$FILE" --standard="$TOP_LEVEL/core/phpcs.xml.dist"
  319. PHPCS=$?
  320. if [ "$PHPCS" -ne "0" ]; then
  321. # If there are failures set the status to a number other than 0.
  322. STATUS=1
  323. else
  324. printf "PHPCS: $FILE ${green}passed${reset}\n"
  325. fi
  326. fi
  327. ############################################################################
  328. ### YAML FILES
  329. ############################################################################
  330. if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.yml$ ]]; then
  331. # Test files with ESLint.
  332. cd "$TOP_LEVEL/core"
  333. node ./node_modules/eslint/bin/eslint.js --quiet --resolve-plugins-relative-to . "$TOP_LEVEL/$FILE"
  334. YAMLLINT=$?
  335. if [ "$YAMLLINT" -ne "0" ]; then
  336. # If there are failures set the status to a number other than 0.
  337. STATUS=1
  338. else
  339. printf "ESLint: $FILE ${green}passed${reset}\n"
  340. fi
  341. cd $TOP_LEVEL
  342. fi
  343. ############################################################################
  344. ### JAVASCRIPT FILES
  345. ############################################################################
  346. if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.js$ ]] && [[ ! $FILE =~ ^core/tests/Drupal/Nightwatch ]] && [[ ! $FILE =~ /tests/src/Nightwatch/ ]] && [[ ! $FILE =~ ^core/modules/ckeditor5/js/ckeditor5_plugins ]]; then
  347. # Work out the root name of the JavaScript so we can ensure that the ES6
  348. # version has been compiled correctly.
  349. if [[ $FILE =~ \.es6\.js$ ]]; then
  350. BASENAME=${FILE%.es6.js}
  351. COMPILE_CHECK=1
  352. else
  353. BASENAME=${FILE%.js}
  354. # We only need to compile check if the .es6.js file is not also
  355. # changing. This is because the compile check will occur for the
  356. # .es6.js file. This might occur if the compile scripts have changed.
  357. contains_element "$BASENAME.es6.js" "${FILES[@]}"
  358. HASES6=$?
  359. if [ "$HASES6" -ne "0" ]; then
  360. COMPILE_CHECK=1
  361. else
  362. COMPILE_CHECK=0
  363. fi
  364. fi
  365. if [[ "$COMPILE_CHECK" == "1" ]] && [[ -f "$TOP_LEVEL/$BASENAME.es6.js" ]]; then
  366. cd "$TOP_LEVEL/core"
  367. yarn run build:js --check --file "$TOP_LEVEL/$BASENAME.es6.js"
  368. CORRECTJS=$?
  369. if [ "$CORRECTJS" -ne "0" ]; then
  370. # No need to write any output the yarn run command will do this for
  371. # us.
  372. STATUS=1
  373. fi
  374. # Check the coding standards.
  375. if [[ -f ".eslintrc.passing.json" ]]; then
  376. node ./node_modules/eslint/bin/eslint.js --quiet --config=.eslintrc.passing.json "$TOP_LEVEL/$BASENAME.es6.js"
  377. CORRECTJS=$?
  378. if [ "$CORRECTJS" -ne "0" ]; then
  379. # No need to write any output the node command will do this for us.
  380. STATUS=1
  381. fi
  382. fi
  383. cd $TOP_LEVEL
  384. else
  385. # If there is no .es6.js file then there should be unless the .js is
  386. # not really Drupal's.
  387. if ! [[ "$FILE" =~ ^core/assets/vendor ]] && ! [[ "$FILE" =~ ^core/modules/ckeditor5/js/build ]] && ! [[ "$FILE" =~ ^core/scripts/js ]] && ! [[ "$FILE" =~ ^core/scripts/css ]] && ! [[ "$FILE" =~ webpack.config.js$ ]] && ! [[ -f "$TOP_LEVEL/$BASENAME.es6.js" ]] && ! [[ "$FILE" =~ core/modules/ckeditor5/tests/modules/ckeditor5_test/js/build/layercake.js ]]; then
  388. printf "${red}FAILURE${reset} $FILE does not have a corresponding $BASENAME.es6.js\n"
  389. STATUS=1
  390. fi
  391. fi
  392. else
  393. # Check coding standards of Nightwatch files.
  394. if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.js$ ]]; then
  395. cd "$TOP_LEVEL/core"
  396. # Check the coding standards.
  397. if [[ -f ".eslintrc.passing.json" ]]; then
  398. node ./node_modules/eslint/bin/eslint.js --quiet --config=.eslintrc.passing.json "$TOP_LEVEL/$FILE"
  399. CORRECTJS=$?
  400. if [ "$CORRECTJS" -ne "0" ]; then
  401. # No need to write any output the node command will do this for us.
  402. STATUS=1
  403. fi
  404. fi
  405. cd $TOP_LEVEL
  406. fi
  407. fi
  408. ############################################################################
  409. ### CSS FILES
  410. ############################################################################
  411. if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.css$ ]]; then
  412. # Work out the root name of the CSS so we can ensure that the PostCSS
  413. # version has been compiled correctly.
  414. if [[ $FILE =~ \.pcss\.css$ ]]; then
  415. BASENAME=${FILE%.pcss.css}
  416. COMPILE_CHECK=1
  417. else
  418. BASENAME=${FILE%.css}
  419. # We only need to compile check if the .pcss.css file is not also
  420. # changing. This is because the compile check will occur for the
  421. # .pcss.css file. This might occur if the compiled stylesheets have
  422. # changed.
  423. contains_element "$BASENAME.pcss.css" "${FILES[@]}"
  424. HASPOSTCSS=$?
  425. if [ "$HASPOSTCSS" -ne "0" ]; then
  426. COMPILE_CHECK=1
  427. else
  428. COMPILE_CHECK=0
  429. fi
  430. fi
  431. # PostCSS
  432. if [[ "$COMPILE_CHECK" == "1" ]] && [[ -f "$TOP_LEVEL/$BASENAME.pcss.css" ]]; then
  433. cd "$TOP_LEVEL/core"
  434. yarn run build:css --check --file "$TOP_LEVEL/$BASENAME.pcss.css"
  435. CORRECTCSS=$?
  436. if [ "$CORRECTCSS" -ne "0" ]; then
  437. # No need to write any output the yarn run command will do this for
  438. # us.
  439. STATUS=1
  440. fi
  441. cd $TOP_LEVEL
  442. fi
  443. fi
  444. if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.css$ ]] && [[ -f "core/node_modules/.bin/stylelint" ]]; then
  445. BASENAME=${FILE%.css}
  446. # We only need to use stylelint on the .pcss.css file. So if this CSS file
  447. # has a corresponding .pcss don't do stylelint.
  448. if [[ $FILE =~ \.pcss\.css$ ]] || [[ ! -f "$TOP_LEVEL/$BASENAME.pcss.css" ]]; then
  449. cd "$TOP_LEVEL/core"
  450. node_modules/.bin/stylelint --allow-empty-input "$TOP_LEVEL/$FILE"
  451. if [ "$?" -ne "0" ]; then
  452. STATUS=1
  453. else
  454. printf "STYLELINT: $FILE ${green}passed${reset}\n"
  455. fi
  456. cd $TOP_LEVEL
  457. fi
  458. fi
  459. if [[ "$STATUS" == "1" ]]; then
  460. FINAL_STATUS=1
  461. # There is no need to print a failure message. The fail will be described
  462. # already.
  463. else
  464. printf "%s ${green}passed${reset}\n" "$FILE"
  465. fi
  466. # Print a line to separate each file's checks.
  467. printf "\n"
  468. printf -- '-%.0s' {1..100}
  469. printf "\n"
  470. done
  471. if [[ "$FINAL_STATUS" == "1" ]] && [[ "$DRUPALCI" == "1" ]]; then
  472. printf "${red}Drupal code quality checks failed.${reset}\n"
  473. printf "To reproduce this output locally:\n"
  474. printf "* Apply the change as a patch\n"
  475. printf "* Run this command locally: sh ./core/scripts/dev/commit-code-check.sh\n"
  476. printf "OR:\n"
  477. printf "* From the merge request branch\n"
  478. printf "* Run this command locally: sh ./core/scripts/dev/commit-code-check.sh --branch %s\n" "$DRUPAL_VERSION"
  479. fi
  480. exit $FINAL_STATUS