From 3c781245bbf3f45d3cc00a17ac0203c749ac57ff Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Thu, 12 Oct 2023 09:13:07 -0700 Subject: [PATCH] feat: create a linter aspect for flake8 Second commit in "how to add a new linter". --- example/.aspect/cli/config.yaml | 1 + example/lint.bzl | 6 +++ example/lint.sh | 2 +- lint/flake8.bzl | 93 +++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 lint/flake8.bzl diff --git a/example/.aspect/cli/config.yaml b/example/.aspect/cli/config.yaml index b0b2eef7..6d29594e 100644 --- a/example/.aspect/cli/config.yaml +++ b/example/.aspect/cli/config.yaml @@ -7,3 +7,4 @@ plugins: lint_aspects: - //:lint.bzl%eslint - //:lint.bzl%buf + - //:lint.bzl%flake8 diff --git a/example/lint.bzl b/example/lint.bzl index 30741a45..e08ace73 100644 --- a/example/lint.bzl +++ b/example/lint.bzl @@ -2,6 +2,7 @@ load("@aspect_rules_lint//lint:eslint.bzl", "eslint_aspect") load("@aspect_rules_lint//lint:buf.bzl", "buf_lint_aspect") +load("@aspect_rules_lint//lint:flake8.bzl", "flake8_aspect") buf = buf_lint_aspect( config = "@@//:buf.yaml", @@ -11,3 +12,8 @@ eslint = eslint_aspect( binary = "@@//:eslint", config = "@@//:eslintrc", ) + +flake8 = flake8_aspect( + binary = "@@//:flake8", + config = "@@//:.flake8", +) diff --git a/example/lint.sh b/example/lint.sh index 7d8cd24a..12b2711b 100755 --- a/example/lint.sh +++ b/example/lint.sh @@ -13,7 +13,7 @@ if [ "$#" -eq 0 ]; then fi # Produce report files -bazel build --aspects //:lint.bzl%eslint,//:lint.bzl%buf --output_groups=report $@ +bazel build --aspects //:lint.bzl%eslint,//:lint.bzl%buf,//:lint.bzl%flake8 --output_groups=report $@ # Show the results. # `-mtime -1`: only look at files modified in the last day, to mitigate showing stale results of old bazel runs. diff --git a/lint/flake8.bzl b/lint/flake8.bzl new file mode 100644 index 00000000..fece0690 --- /dev/null +++ b/lint/flake8.bzl @@ -0,0 +1,93 @@ +"""API for declaring a flake8 lint aspect that visits py_library rules. + +Typical usage: + +``` +load("@aspect_rules_lint//lint:flake8.bzl", "flake8_aspect") + +flake8 = flake8_aspect( + binary = "@@//:flake8", + config = "@@//:.flake8", +) +``` +""" + +def flake8_action(ctx, executable, srcs, config, report, use_exit_code = False): + """Run flake8 as an action under Bazel. + + Based on https://flake8.pycqa.org/en/latest/user/invocation.html + + Args: + ctx: Bazel Rule or Aspect evaluation context + executable: label of the the flake8 program + srcs: python files to be linted + config: label of the flake8 config file (setup.cfg, tox.ini, or .flake8) + report: output file to generate + use_exit_code: whether to fail the build when a lint violation is reported + """ + inputs = srcs + [config] + outputs = [report] + + # Wire command-line options, see + # https://flake8.pycqa.org/en/latest/user/options.html + args = ctx.actions.args() + args.add_all(srcs) + args.add(report, format="--output-file=%s") + args.add(config, format="--config=%s") + if not use_exit_code: + args.add("--exit-zero") + + ctx.actions.run( + inputs = inputs, + outputs = outputs, + executable = executable, + arguments = [args], + mnemonic = "flake8", + ) + +# buildifier: disable=function-docstring +def _flake8_aspect_impl(target, ctx): + if ctx.rule.kind in ["py_library"]: + report = ctx.actions.declare_file(target.label.name + ".flake8-report.txt") + flake8_action(ctx, ctx.executable._flake8, ctx.rule.files.srcs, ctx.file._config_file, report) + results = depset([report]) + else: + results = depset() + + return [ + OutputGroupInfo(report = results), + ] + +def flake8_aspect(binary, config): + """A factory function to create a linter aspect. + + Attrs: + binary: a flake8 executable. Can be obtained from rules_python like so: + + ``` + load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + + py_console_script_binary( + name = "flake8", + pkg = "@pip//flake8:pkg", + ) + ``` + config: the flake8 config file (`setup.cfg`, `tox.ini`, or `.flake8`) + """ + return aspect( + implementation = _flake8_aspect_impl, + # Edges we need to walk up the graph from the selected targets. + # Needed for linters that need semantic information like transitive type declarations. + # attr_aspects = ["deps"], + attrs = { + "_flake8": attr.label( + default = binary, + executable = True, + cfg = "exec", + ), + "_config_file": attr.label( + default = config, + allow_single_file = True, + ), + }, + )