diff --git a/.github/workflows/lint_random_files.yml b/.github/workflows/lint_random_files.yml index df52a3fb75a2..7f4cdc825602 100644 --- a/.github/workflows/lint_random_files.yml +++ b/.github/workflows/lint_random_files.yml @@ -302,70 +302,19 @@ jobs: - name: 'Lint JavaScript files' id: lint-javascript if: ( github.event.inputs.javascript != 'false' ) && ( success() || failure() ) + env: + FIX: ${{ github.event.inputs.fix == 'true' && '1' || '0' }} + QUIET: '0' + ERR_FILE: ${{ github.workspace }}/lint_javascript_errors.txt run: | - # If any command in a pipeline fails, the entire pipeline should fail: - set -o pipefail - - # Determine root directory: - root=$(git rev-parse --show-toplevel) - - # Define the path to ESLint configuration file for linting examples: - eslint_examples_conf="${root}/etc/eslint/.eslintrc.examples.js" - - # Define the path to ESLint configuration file for linting tests: - eslint_tests_conf="${root}/etc/eslint/.eslintrc.tests.js" + # Get JavaScript files to lint: + files=$(echo "${{ steps.random-files.outputs.files }}" | tr ',' '\n' | grep '\.js$' | tr '\n' ' ') - # Define the path to ESLint configuration file for linting benchmarks: - eslint_benchmarks_conf="${root}/etc/eslint/.eslintrc.benchmarks.js" - - # Set `FIX` variable to `1` to enable automatic fixing of lint errors, `0` to disable: - if [ "${{ github.event.inputs.fix }}" == "true" ]; then - FIX=1 + # Run the lint script: + if [ -n "${files}" ]; then + "$GITHUB_WORKSPACE/.github/workflows/scripts/lint_javascript_files_error_log" ${files} else - FIX=0 - fi - - # Combined error file: - ERR_FILE="lint_javascript_errors.txt" - > "$ERR_FILE" # Initialize a clean file - - # Lint JavaScript source files: - files=$(echo "${{ steps.random-files.outputs.files }}" | tr ',' '\n' | grep '\.js$' | grep -v -e '/examples' -e '/test' -e '/benchmark' -e '^dist/' | tr '\n' ' ') - - # Build native addons if present: - packages=$(echo "${files}" | tr ' ' '\n' | sed 's/^lib\/node_modules\///g' | sed 's/\/lib\/.*//g' | sort | uniq) - for pkg in ${packages}; do - if [ -f "lib/node_modules/${pkg}/binding.gyp" ]; then - NODE_ADDONS_PATTERN="${pkg}" make install-node-addons - fi - done - - if [[ -n "${files}" ]]; then - make lint-javascript-files FIX="${FIX}" FAST_FAIL=0 FILES="${files}" 2>&1 | tee -a "$ERR_FILE" - fi - - # Lint JavaScript command-line interfaces... - file=$(echo "${{ steps.random-files.outputs.files }}" | tr ',' '\n' | grep '\.js$' | grep -E '/bin/cli$' | tr '\n' ' ') - if [[ -n "${file}" ]]; then - make lint-javascript-files FIX="${FIX}" FAST_FAIL=0 FILES="${file}" 2>&1 | tee -a "$ERR_FILE" - fi - - # Lint JavaScript example files: - files=$(echo "${{ steps.random-files.outputs.files }}" | tr ',' '\n' | grep '/examples/.*\.js$' | tr '\n' ' ') - if [[ -n "${files}" ]]; then - make lint-javascript-files FIX="${FIX}" FAST_FAIL=0 FILES="${files}" ESLINT_CONF="${eslint_examples_conf}" 2>&1 | tee -a "$ERR_FILE" - fi - - # Lint JavaScript test files: - files=$(echo "${{ steps.random-files.outputs.files }}" | tr ',' '\n' | grep '/test/.*\.js$' | tr '\n' ' ') - if [[ -n "${files}" ]]; then - make lint-javascript-files FIX="${FIX}" FAST_FAIL=0 FILES="${files}" ESLINT_CONF="${eslint_tests_conf}" 2>&1 | tee -a "$ERR_FILE" - fi - - # Lint JavaScript benchmark files: - files=$(echo "${{ steps.random-files.outputs.files }}" | tr ',' '\n' | grep '/benchmark/.*\.js$' | tr '\n' ' ') - if [[ -n "${files}" ]]; then - make lint-javascript-files FIX="${FIX}" FAST_FAIL=0 FILES="${files}" ESLINT_CONF="${eslint_benchmarks_conf}" 2>&1 | tee -a "$ERR_FILE" + echo "No JavaScript files to lint." fi # Create sub-issue for JavaScript lint failures: diff --git a/.github/workflows/scripts/lint_javascript_files_error_log b/.github/workflows/scripts/lint_javascript_files_error_log new file mode 100755 index 000000000000..c7ce4c94a7e6 --- /dev/null +++ b/.github/workflows/scripts/lint_javascript_files_error_log @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# +# @license Apache-2.0 +# +# Copyright (c) 2025 The Stdlib Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Script to lint JavaScript files with extended error handling and reporting. +# +# Usage: lint_javascript_files_extended file1 [file2 file3 ...] +# +# Arguments: +# +# file1 File path. +# file2 File path. +# file3 File path. +# +# Environment variables: +# +# FIX 0 or 1 indicating whether to automatically fix errors. +# QUIET 0 or 1 indicating whether to run in quiet mode (errors only). +# ERR_FILE Path to error output file (optional). +# + +# shellcheck disable=SC2086 + +# Ensure that the exit status of pipelines is non-zero in the event that at least one of the commands in a pipeline fails: +set -o pipefail + + +# VARIABLES # + +# Determine root directory: +root=$(git rev-parse --show-toplevel) + +# Flag indicating whether to automatically fix errors: +fix="${FIX:-0}" + +# Flag indicating whether to run in quiet mode: +quiet="${QUIET:-0}" + +# Error output file: +err_file="${ERR_FILE:-${TMPDIR:-/tmp}/lint_javascript_errors_$$.txt}" + +# Files to lint: +files_to_lint="$*" + +# Define the path to ESLint configuration file for linting examples: +eslint_examples_conf="${root}/etc/eslint/.eslintrc.examples.js" + +# Define the path to ESLint configuration file for linting tests: +eslint_tests_conf="${root}/etc/eslint/.eslintrc.tests.js" + +# Define the path to ESLint configuration file for linting benchmarks: +eslint_benchmarks_conf="${root}/etc/eslint/.eslintrc.benchmarks.js" + +# Initialize error flag: +error_occurred=0 + + +# FUNCTIONS # + +# Runs make lint command with error handling. +# +# $1 - files to lint +# $2 - additional make arguments +# $3 - ESLint configuration argument +run_make_lint() { + local files_to_lint="$1" + local make_args="$2" + local eslint_conf_arg="$3" + + local eslint_flags="" + if [ "${quiet}" -eq 1 ]; then + eslint_flags="ESLINT_FLAGS='--quiet'" + fi + + local make_cmd="make lint-javascript-files FIX=${fix} FAST_FAIL=0 FILES=\"${files_to_lint}\" ${make_args} ${eslint_conf_arg} ${eslint_flags}" + + echo "Running: ${make_cmd}" + if ! eval "${make_cmd}"; then + if [ "${quiet}" -eq 0 ]; then + echo "Initial linting failed. Retrying with --quiet and logging errors..." + eval "make lint-javascript-files FIX=${fix} FAST_FAIL=0 FILES=\"${files_to_lint}\" ${make_args} ${eslint_conf_arg} ESLINT_FLAGS='--quiet'" >> "${err_file}" 2>&1 + else + # Already running with --quiet, just log the error + echo "Linting failed for files." >> "${err_file}" + fi + error_occurred=1 + fi +} + + +# MAIN # + +# Initialize error file: +: > "${err_file}" + +# Build native addons if present: +packages=$(echo "${files_to_lint}" | tr ' ' '\n' | grep '^lib/node_modules/' | sed 's/^lib\/node_modules\///g' | sed 's/\/lib\/.*//g' | sort | uniq) +for pkg in ${packages}; do + if [ -f "lib/node_modules/${pkg}/binding.gyp" ]; then + echo "Building native addon for ${pkg}..." + NODE_ADDONS_PATTERN="${pkg}" make install-node-addons + fi +done + +# Lint JavaScript source files: +files=$(echo "${files_to_lint}" | tr ' ' '\n' | grep '\.js$' | grep -v -e '/examples' -e '/test' -e '/benchmark' -e '^dist/' | tr '\n' ' ' | sed 's/ $//') +if [ -n "${files}" ]; then + echo "Linting JavaScript source files..." + run_make_lint "${files}" "" "" +fi + +# Lint JavaScript command-line interfaces: +files=$(echo "${files_to_lint}" | tr ' ' '\n' | grep '\.js$' | grep -E '/bin/cli$' | tr '\n' ' ' | sed 's/ $//') +if [ -n "${files}" ]; then + echo "Linting JavaScript CLI files..." + run_make_lint "${files}" "" "" +fi + +# Lint JavaScript example files: +files=$(echo "${files_to_lint}" | tr ' ' '\n' | grep '/examples/.*\.js$' | tr '\n' ' ' | sed 's/ $//') +if [ -n "${files}" ]; then + echo "Linting JavaScript example files..." + run_make_lint "${files}" "" "ESLINT_CONF=\"${eslint_examples_conf}\"" +fi + +# Lint JavaScript test files: +files=$(echo "${files_to_lint}" | tr ' ' '\n' | grep '/test/.*\.js$' | tr '\n' ' ' | sed 's/ $//') +if [ -n "${files}" ]; then + echo "Linting JavaScript test files..." + run_make_lint "${files}" "" "ESLINT_CONF=\"${eslint_tests_conf}\"" +fi + +# Lint JavaScript benchmark files: +files=$(echo "${files_to_lint}" | tr ' ' '\n' | grep '/benchmark/.*\.js$' | tr '\n' ' ' | sed 's/ $//') +if [ -n "${files}" ]; then + echo "Linting JavaScript benchmark files..." + run_make_lint "${files}" "" "ESLINT_CONF=\"${eslint_benchmarks_conf}\"" +fi + +# Report results: +if [ "${error_occurred}" -eq 1 ]; then + echo "JavaScript linting errors occurred. See details below:" + cat "${err_file}" + + # Clean up temp file ONLY if it was auto-generated (ERR_FILE not provided): + if [ -z "${ERR_FILE}" ]; then + rm -f "${err_file}" + fi + + exit 1 +else + echo "JavaScript linting completed successfully." + + # Clean up temp file ONLY if it was auto-generated (ERR_FILE not provided): + if [ -z "${ERR_FILE}" ]; then + rm -f "${err_file}" + fi + + exit 0 +fi diff --git a/tools/make/lib/lint/javascript/eslint.mk b/tools/make/lib/lint/javascript/eslint.mk index 2d193bdc6421..eda192fb5cb8 100644 --- a/tools/make/lib/lint/javascript/eslint.mk +++ b/tools/make/lib/lint/javascript/eslint.mk @@ -45,23 +45,29 @@ ESLINT_CONF_BENCHMARKS ?= $(CONFIG_DIR)/eslint/.eslintrc.benchmarks.js ESLINT_IGNORE ?= $(ROOT_DIR)/.eslintignore # Define the command-line options to use when invoking the ESLint executable: -ESLINT_FLAGS ?= \ +eslint_flags := \ --ignore-path $(ESLINT_IGNORE) \ --report-unused-disable-directives +# Define user-supplied command-line options: +ESLINT_FLAGS ?= + ifeq ($(AUTOFIX),true) - ESLINT_FLAGS += --fix + eslint_flags += --fix endif FIX_TYPE ?= ifneq ($(FIX_TYPE),) - ESLINT_FLAGS += --fix-type $(FIX_TYPE) + eslint_flags += --fix-type $(FIX_TYPE) else ifeq ($(AUTOFIX),true) - ESLINT_FLAGS += --fix-type problem,layout,directive + eslint_flags += --fix-type problem,layout,directive endif endif +# Append user-supplied command-line options: +eslint_flags += $(ESLINT_FLAGS) + # RULES # #/ @@ -88,14 +94,14 @@ ifeq ($(FAIL_FAST), true) $(QUIET) $(FIND_SOURCES_CMD) | grep '^[\/]\|^[a-zA-Z]:[/\]' | while read -r file; do \ echo ''; \ echo "Linting file: $$file"; \ - $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF) $$file || exit 1; \ + $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF) $$file || exit 1; \ done else $(QUIET) status=0; \ $(FIND_SOURCES_CMD) | grep '^[\/]\|^[a-zA-Z]:[/\]' | while read -r file; do \ echo ''; \ echo "Linting file: $$file"; \ - if ! $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF) $$file; then \ + if ! $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF) $$file; then \ echo 'Linting failed.'; \ status=1; \ fi; \ @@ -129,14 +135,14 @@ ifeq ($(FAIL_FAST), true) $(QUIET) $(FIND_TESTS_CMD) | grep '^[\/]\|^[a-zA-Z]:[/\]' | while read -r file; do \ echo ''; \ echo "Linting file: $$file"; \ - $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF_TESTS) $$file || exit 1; \ + $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF_TESTS) $$file || exit 1; \ done else $(QUIET) status=0; \ $(FIND_TESTS_CMD) | grep '^[\/]\|^[a-zA-Z]:[/\]' | while read -r file; do \ echo ''; \ echo "Linting file: $$file"; \ - if ! $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF_TESTS) $$file; then \ + if ! $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF_TESTS) $$file; then \ echo 'Linting failed.'; \ status=1; \ fi; \ @@ -170,14 +176,14 @@ ifeq ($(FAIL_FAST), true) $(QUIET) $(FIND_EXAMPLES_CMD) | grep '^[\/]\|^[a-zA-Z]:[/\]' | while read -r file; do \ echo ''; \ echo "Linting file: $$file"; \ - $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF_EXAMPLES) $$file || exit 1; \ + $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF_EXAMPLES) $$file || exit 1; \ done else $(QUIET) status=0; \ $(FIND_EXAMPLES_CMD) | grep '^[\/]\|^[a-zA-Z]:[/\]' | while read -r file; do \ echo ''; \ echo "Linting file: $$file"; \ - if ! $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF_EXAMPLES) $$file; then \ + if ! $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF_EXAMPLES) $$file; then \ echo 'Linting failed.'; \ status=1; \ fi; \ @@ -211,14 +217,14 @@ ifeq ($(FAIL_FAST), true) $(QUIET) $(FIND_BENCHMARKS_CMD) | grep '^[\/]\|^[a-zA-Z]:[/\]' | while read -r file; do \ echo ''; \ echo "Linting file: $$file"; \ - $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF_BENCHMARKS) $$file || exit 1; \ + $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF_BENCHMARKS) $$file || exit 1; \ done else $(QUIET) status=0; \ $(FIND_BENCHMARKS_CMD) | grep '^[\/]\|^[a-zA-Z]:[/\]' | while read -r file; do \ echo ''; \ echo "Linting file: $$file"; \ - if ! $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF_BENCHMARKS) $$file; then \ + if ! $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF_BENCHMARKS) $$file; then \ echo 'Linting failed.'; \ status=1; \ fi; \ @@ -249,14 +255,14 @@ ifeq ($(FAIL_FAST), true) $(QUIET) for file in $(FILES); do \ echo ''; \ echo "Linting file: $$file"; \ - $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF) $$file || exit 1; \ + $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF) $$file || exit 1; \ done else $(QUIET) status=0; \ for file in $(FILES); do \ echo ''; \ echo "Linting file: $$file"; \ - if ! $(ESLINT) $(ESLINT_FLAGS) --config $(ESLINT_CONF) $$file; then \ + if ! $(ESLINT) $(eslint_flags) --config $(ESLINT_CONF) $$file; then \ echo 'Linting failed.'; \ status=1; \ fi; \