blob: df9011daf513ed73999aee775e990daca7e56e90 [file] [log] [blame] [edit]
# Copyright 2023 The Pigweed 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
#
# https://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.
def _get_affected_python_files(ctx):
files = [f for f in ctx.scm.affected_files() if f.endswith(".py")]
exclusions = [
# recipes.py is vendored from the recipe engine and should be ignored.
"recipes.py",
# cleanup_deps.py and renumber_proto_fields.py are vendored from the
# upstream Fuchsia recipes repo as-is and should not be modified.
"scripts/cleanup_deps.py",
"scripts/renumber_proto_fields.py",
]
return [f for f in files if f not in exclusions]
def ruff_format(ctx):
tools_dir = _install_tools(ctx)
py_files = _get_affected_python_files(ctx)
if not py_files:
return
original_contents = {}
# First run `ruff check --fix --select=I` to sort imports, which `ruff
# format` doesn't support.
isort_procs = {}
for filepath in py_files:
original = str(ctx.io.read_file(filepath))
original_contents[filepath] = original
isort_procs[filepath] = ctx.os.exec(
[
tools_dir + "/ruff",
"check",
"--fix",
# Only check import sorting.
"--select=I",
"--stdin-filename",
filepath,
"-",
],
stdin = original,
)
# Then run `ruff format`.
format_procs = {}
for filepath, proc in isort_procs.items():
res = proc.wait()
format_procs[filepath] = ctx.os.exec(
[
tools_dir + "/ruff",
"format",
"--stdin-filename",
filepath,
"-",
],
stdin = res.stdout,
)
for filepath, proc in format_procs.items():
res = proc.wait()
original = original_contents[filepath]
if res.stdout != original:
ctx.emit.finding(
filepath = filepath,
level = "error",
replacements = [res.stdout],
)
def ruff_lint(ctx):
tools_dir = _install_tools(ctx)
py_files = _get_affected_python_files(ctx)
if not py_files:
return
res = ctx.os.exec(
[
tools_dir + "/ruff",
"check",
"--output-format",
"json",
"--show-fixes",
] +
py_files,
ok_retcodes = [0, 1],
).wait()
for finding in json.decode(res.stdout):
filepath = finding["filename"]
if filepath.startswith(ctx.scm.root):
filepath = filepath[len(ctx.scm.root) + 1:]
replacements = None
if finding.get("fix") and len(finding["fix"].get("edits", [])) == 1:
edit = finding["fix"]["edits"][0]
start_loc, end_loc = edit["location"], edit["end_location"]
replacements = [edit["content"]]
else:
start_loc, end_loc = finding["location"], finding["end_location"]
ctx.emit.finding(
filepath = filepath,
message = "%s: %s" % (finding["code"], finding["message"]),
level = "error",
line = start_loc["row"],
col = start_loc["column"],
end_line = end_loc["row"],
end_col = end_loc["column"],
replacements = replacements,
)
def proto_format(ctx):
tools_dir = _install_tools(ctx)
procs = []
for p in ctx.scm.affected_files():
if not p.endswith(".proto"):
continue
# TODO(olivernewman): Use a single `buf format` invocation and the
# --diff option once shac knows how to parse diffs.
cmd = [
tools_dir + "/buf",
"format",
"--exit-code",
p,
]
procs.append((p, ctx.os.exec(cmd, ok_retcodes = [0, 100])))
for p, proc in procs:
res = proc.wait()
if res.retcode == 100:
ctx.emit.finding(
filepath = p,
level = "error",
replacements = [res.stdout],
)
def proto_field_numbering(ctx):
"""Checks that recipe property protobuf files have clean field numbers.
Recipe property protos need not subscribe to normal protobuf maintenance
conventions such as never deleting fields or changing field numbers, because
they are only used to de/serialize JSON-encoded protos, never binary-encoded
protos.
So it's safe (and required, for code cleanliness) to keep proto fields
monotonically increasing with no jumps.
Args:
ctx: A ctx instance.
"""
for f in ctx.scm.affected_files():
# The recipe_proto directory contains protos that may be used in other
# places than just recipe properties, so we cannot safely renumber their
# fields.
if not f.endswith(".proto") or f.startswith("recipe_proto/"):
continue
res = ctx.os.exec(
[
"python3",
"scripts/renumber_proto_fields.py",
"--dry-run",
f,
],
).wait()
if res.stdout != str(ctx.io.read_file(f)):
ctx.emit.finding(
filepath = f,
level = "error",
replacements = [res.stdout],
)
def _install_tools(ctx):
install_dir = ctx.scm.root + "/.tools"
ctx.os.exec(
[ctx.scm.root + "/scripts/install-shac-tools.sh", install_dir],
allow_network = True,
).wait()
return install_dir
def check_deps(ctx):
res = ctx.os.exec(
[
"python3",
"scripts/cleanup_deps.py",
"--check",
"--json-output",
"-",
],
ok_retcodes = [0, 65],
).wait()
if res.retcode == 65:
for file in json.decode(res.stdout):
# TODO(olivernewman): Parse the diff so fixes can be applied with
# `shac fix`.
ctx.emit.finding(
filepath = file,
message = "DEPS are malformatted. Run ./scripts/cleanup_deps.py to fix.",
level = "error",
)
shac.register_check(shac.check(ruff_format, formatter = True))
shac.register_check(shac.check(proto_format, formatter = True))
shac.register_check(shac.check(proto_field_numbering, formatter = True))
shac.register_check(ruff_lint)
shac.register_check(check_deps)