blob: 673fc31003a41a4fbde30eee5c2d7de46992d330 [file] [log] [blame] [edit]
# Copyright 2023 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
def _get_affected_python_files(ctx):
return [
f
for f in ctx.scm.affected_files()
if f.endswith(".py") and
# recipes.py is vendored from the recipe engine and should be
# ignored.
f != "recipes.py"
]
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",
)
def recipe_style_guide(ctx):
"""Enforces http://go/fuchsia-recipe-docs#style-guide."""
procs = []
for f in ctx.scm.affected_files():
if f.endswith(".json") and ".expected" in f:
test_name = f.split("/")[-1].rsplit(".", 1)[0]
if " " in test_name:
# It would be nicer to parse the recipe file to find test case
# names because then we could emit the finding at the place
# where the test name is defined. But because tests are
# generated by arbitrary Python code, it's practically
# impossible to parse out their names in a foolproof way.
ctx.emit.finding(
filepath = f,
message = "Test name %r should not contain spaces" % test_name,
level = "error",
)
if not f.endswith(".py"):
continue
procs.append(ctx.os.exec(["python3", "scripts/enforce_style_guide.py", f]))
for proc in procs:
res = proc.wait()
for finding in json.decode(res.stdout):
ctx.emit.finding(**finding)
def prohibited_properties(ctx):
""" Checks that recipe protobufs do not contain prohibited properties
https://crrev.com/783ca64715fa405f0a4339d1ecd8b5a8d49caa7f/buildbucket/appengine/rpc/schedule_build.go#223
Recipes with prohibited properties can cause issues with led builds after
migrating to real builds because properties with prohibited names can not
be changed with `led edit`.
"""
# Renaming schema for prohibited properties.
# Each occurrence of a prohibited property (keys) must be replaced with its corresponding value.
# We only need to be concerned with properties that can be set in protobufs.
renaming_schema = {
"repository": "remote",
"branch": "git_branch",
"buildername": "builder_name",
"buildbucket": "build_bucket",
}
for file in ctx.scm.affected_files():
if not file.endswith(".proto"):
continue
if file.startswith("recipe_modules/"):
continue
contents = str(ctx.io.read_file(file))
for i, line in enumerate(contents.splitlines()):
# Check line for declaration of prohibited properties.
# Only check for top level properties.
match = ctx.re.match(r"^ (\w+) (\w+) =", line)
if not match:
continue
property_name = match.groups[2]
if property_name not in renaming_schema:
continue
ctx.emit.finding(
level = "error",
filepath = file,
line = i + 1,
col = len(match.groups[1]) + 4,
end_col = len(match.groups[1]) + 4 + len(match.groups[2]),
message = "%r is a prohibited property. Replace this with %r." % (
property_name,
renaming_schema[property_name],
),
replacements = [renaming_schema[property_name]],
)
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(prohibited_properties)
shac.register_check(ruff_lint)
shac.register_check(check_deps)
shac.register_check(recipe_style_guide)