sourcegraph/dev/eslint.bzl
Greg Magolan 27da7890fc
chore(bazel): fixup custom eslint test rule after bump to rules_js 2 (#63143)
This regressed in https://github.com/sourcegraph/sourcegraph/pull/63022
where the custom `gather_files_from_js_providers` function that was
copied over from rules_js 1.x was including runfiles.

Resolves eslint failures seen in
https://buildkite.com/sourcegraph/sourcegraph/builds/277072#018fe743-abac-44d8-911b-d5a7ed425413
and observed locally:

```
(07:19:15) INFO: From ESLint client/wildcard/wildcard_lib_eslint-output.txt:
  |  
  | Oops! Something went wrong! :(
  |  
  | ESLint: 8.57.0
  |  
  | ESLint couldn't find a configuration file. To set up a configuration file for this project, please run:
  |  
  | npm init @eslint/config
  |  
  | ESLint looked for configuration files in /tmp/bazel-working-directory/__main__/bazel-out/k8-fastbuild/bin/client/wildcard/src/components/Alert and its ancestors. If it found none, it then looked in your home directory.
  |  
  | If you think you already have a configuration file or if you need more help, please stop by the ESLint Discord server: https://eslint.org/chat

```

## Test plan

CI (check test logs)

## Changelog
2024-06-06 23:32:34 +01:00

210 lines
7.9 KiB
Python

load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "COPY_FILE_TO_BIN_TOOLCHAINS", "copy_files_to_bin_actions")
load("@aspect_rules_js//js:defs.bzl", "js_library")
load("@aspect_rules_js//js:libs.bzl", "js_lib_helpers")
load("@aspect_rules_js//js:providers.bzl", "JsInfo")
def eslint_config_and_lint_root(name = "eslint_config", config_deps = [], root_js_deps = []):
"""
Creates an ESLint configuration target and an ESLint test target for a client package root JS files.
Args:
name: The name of the ESLint configuration target.
config_deps: A list of dependencies for the ESLint config target.
root_js_deps: A list of dependencies for the `root_js_eslint` target.
The macro assumes the presence of specific files (".eslintrc.js", ".eslintignore", "package.json")
and a tsconfig target in the current directory. It adds a reference to a top-level ESLint configuration
as a dependency, and sets the visibility to the current package and its subpackages.
For the 'root_js_eslint' target, it assumes all '.js' files in the current directory as its sources and
additional dependencies provided by 'root_js_deps'. It uses a global ESLint binary and the generated
ESLint configuration as its config.
Example usage:
eslint_config_and_lint_root(
config_deps = ["//my:dependency"],
root_js_deps = ["//other:dependency"],
)
"""
js_library(
name = name,
testonly = True,
srcs = ["//:eslint_config"],
data = [
"package.json",
":tsconfig",
],
deps = [
"//:eslint_config",
] + config_deps,
visibility = ["//{}:__subpackages__".format(get_client_package_path())],
)
eslint_test_with_types(
name = "root_js_eslint",
srcs = native.glob(["*.js"]),
config = ":eslint_config",
deps = [
"//:node_modules/@types/node",
] + root_js_deps,
)
# This private rule implementation wraps the ESLint binary.
# It executes ESLint against the provided source files and
# ensures that depenencies' type are available at lint time.
def _custom_eslint_impl(ctx):
copied_srcs = copy_files_to_bin_actions(ctx, ctx.files.srcs)
input_targets = [ctx.attr.config] + ctx.attr.deps
input_depsets = [depset(
copied_srcs + [ctx.executable.binary],
transitive = [js_lib_helpers.gather_files_from_js_infos(
targets = input_targets,
include_sources = True, # include sources & transitives sources so the eslint config .js files are picked up
include_types = True, # we have to include types because we need to lint the types.
include_transitive_sources = True, # include sources & transitives sources so the eslint config .js files are picked up
include_transitive_types = True, # we have to include types because we need to lint the types.
include_npm_sources = True,
)],
)]
# include runfiles in inputs as well so they can be resolved from the execroot
input_depsets.extend([
target[DefaultInfo].default_runfiles.files
for target in input_targets
if DefaultInfo in target and hasattr(target[DefaultInfo], "default_runfiles")
])
runfiles = js_lib_helpers.gather_runfiles(
ctx = ctx,
sources = [],
data = [ctx.attr.config],
deps = [],
)
# Declare the output file for the ESLint output.
report = ctx.actions.declare_file(ctx.attr.report)
args = ctx.actions.args() # Create the argument list for the ESLint command.
args.add("--quiet") # Ignore warnings and fail only on errors.
args.add_all(["--format", "./{}".format(ctx.files.formatter[0].short_path)]) # Use the custom formatter to ouput relative paths.
args.add_all([s.short_path for s in copied_srcs]) # Specify the files to lint.
args.add_all(["--output-file", report.short_path]) # Specify the output file for the ESLint output.
# Declare the output file for the exit code output.
exit_code_out = ctx.actions.declare_file("exit_%s" % ctx.attr.report)
env = {
"BAZEL_BINDIR": ctx.bin_dir.path,
# "JS_BINARY__LOG_DEBUG": "1",
# "JS_BINARY__LOG_INFO": "1",
# "JS_BINARY__LOG_ERROR": "1",
# "JS_BINARY__SILENT_ON_SUCCESS": "0",
# "JS_BINARY__STDOUT_OUTPUT_FILE": report.path,
# "JS_BINARY__STDERR_OUTPUT_FILE": report.path,
"JS_BINARY__EXIT_CODE_OUTPUT_FILE": exit_code_out.path,
}
# The script wrapper around the ESLint binary is essential to create an empty 'report'
# file in cases where ESLint finds no errors. Bazel expects all declared outputs of
# ctx.actions.run_shell to be created during its execution. Failure to do so results
# in Bazel errors, hence if ESLint doesn't generate a 'report', we manually create one.
command = """
#!/usr/bin/env bash
set -o pipefail -o errexit -o nounset
# Call the ESLint @aspect_rules_js wrapper.
"{binary}" "$@"
# If the ESLint report is not created, create the empty one.
if [ ! -f "{report}" ]; then
touch "{report}"
fi
""".format(binary = ctx.executable.binary.path, report = report.path)
# Generate and run a bash script to wrap the binary
ctx.actions.run_shell(
env = env,
inputs = depset([], transitive = input_depsets),
outputs = [report, exit_code_out],
command = command,
arguments = [args],
mnemonic = "ESLint",
tools = ctx.attr.binary[DefaultInfo].default_runfiles.files,
)
return [
DefaultInfo(
files = depset([report]),
runfiles = runfiles,
),
OutputGroupInfo(
report = depset([report]),
runfiles = runfiles.files,
),
]
_eslint_test_with_types = rule(
implementation = _custom_eslint_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
"deps": attr.label_list(default = [], providers = [JsInfo]),
"config": attr.label(allow_single_file = True),
"formatter": attr.label(allow_single_file = True, default = Label("//:eslint-relative-formatter")),
"binary": attr.label(executable = True, cfg = "exec", allow_files = True),
"report": attr.string(),
},
toolchains = COPY_FILE_TO_BIN_TOOLCHAINS,
)
def eslint_test_with_types(name, **kwargs):
"""
A higher-level function to perform an ESLint test on TypeScript files with type checking.
Args:
name: A string representing the name of the test.
**kwargs: Arbitrary keyword arguments for additional customization of the test. This can
include the source files (`srcs`), dependencies (`deps`), ESLint configuration
(`config`), and more.
This macro wraps the `_eslint_test_with_types` rule and subsequently runs a shell test to
verify the output. It generates an output report named '<name>-output.txt' and a linting
target with the name of the original test suffixed with '_lint'.
Example usage:
eslint_test_with_types(
name = "my_test",
srcs = ["my_file.ts"],
deps = [":my_dependency"],
testonly = True,
config = ":my_eslint_config",
)
"""
lint_name = "%s_lint" % name
report = "%s-output.txt" % name
_eslint_test_with_types(
testonly = True,
name = lint_name,
report = report,
binary = "//:eslint",
**kwargs
)
lint_target_name = ":%s" % lint_name
native.sh_test(
name = name,
srcs = ["//dev:eslint-report-test.sh"],
args = ["$(location %s)" % lint_target_name],
data = [lint_target_name],
timeout = "short",
)
# This function provides the path to the client package, assuming
# that eslint config files are located at `client/<package>`.
def get_client_package_path():
return "/".join(native.package_name().split("/")[:2])