blob: 5cf216be5cdbfda18955b1a76a8311697af870c0 [file] [log] [blame]
# Copyright 2023 The Shac 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.
load("common.star", "go_env", "go_install")
def _gofmt(ctx, simplify = True):
"""Runs gofmt on a Go code base.
Args:
ctx: A ctx instance.
simplify: Whether to set the -s flag on gofmt.
"""
go_files = [f for f in ctx.scm.affected_files() if f.endswith(".go")]
if not go_files:
return
base_cmd = ["gofmt"]
if simplify:
base_cmd.append("-s")
unformatted = ctx.os.exec(base_cmd + ["-l"] + go_files).wait().stdout.splitlines()
for f in unformatted:
new_contents = ctx.os.exec(base_cmd + [f]).wait().stdout
ctx.emit.finding(
level = "error",
message = "needs formatting",
filepath = f,
replacements = [new_contents],
)
gofmt = shac.check(_gofmt, formatter = True)
def _gosec(ctx, version = "v2.15.0", level = "error", exclude = [
# shac checks are allowed to run arbitrary subprocesses, so it's common for
# shac's source code to run non-constant subcommands.
"G204",
# shac checks are allowed to read arbitrary files, so it's common for shac's
# source code to read non-constant files.
"G304",
]):
"""Runs gosec on a Go code base.
See https://github.com/securego/gosec for more details.
Args:
ctx: A ctx instance.
version: gosec version to install. Defaults to a recent version, that will
be rolled from time to time.
level: level at which issues should be emitted.
"""
affected_files = set(ctx.scm.affected_files())
exe = go_install(ctx, "github.com/securego/gosec/v2/cmd/gosec", version)
res = ctx.os.exec(
[exe, "-fmt=json", "-quiet", "-exclude=%s" % ",".join(exclude), "-exclude-dir=.tools", "./..."],
ok_retcodes = (0, 1),
env = go_env(ctx, "gosec"),
).wait()
if not res.retcode:
return
# Schema is https://pkg.go.dev/github.com/securego/gosec/v2#ReportInfo
d = json.decode(res.stdout)
o = len(ctx.scm.root) + 1
for file, errs in d["Golang errors"].items():
filepath = file[o:]
if filepath not in affected_files:
continue
for e in errs:
ctx.emit.finding(
level = "error",
message = e["error"],
filepath = filepath,
line = int(e["line"]),
col = int(e["column"]),
)
for i in d["Issues"]:
line = i["line"].split("-")[0]
filepath = i["file"][o:]
if filepath not in affected_files:
continue
ctx.emit.finding(
level = level,
message = i["rule_id"] + ": " + i["details"],
filepath = filepath,
line = int(line),
col = int(i["column"]),
)
gosec = shac.check(_gosec)
def _ineffassign(ctx, version = "v0.0.0-20230107090616-13ace0543b28"):
"""Runs ineffassign on a Go code base.
See https://github.com/gordonklaus/ineffassign for more details.
Args:
ctx: A ctx instance.
version: ineffassign version to install. Defaults to a recent version, that
will be rolled from time to time.
"""
exe = go_install(ctx, "github.com/gordonklaus/ineffassign", version)
res = ctx.os.exec(
[exe, "./..."],
env = go_env(ctx, "ineffassign"),
# ineffassign's README claims that it emits a retcode of 1 if it returns any
# findings, but it actually emits a retcode of 3.
# https://github.com/gordonklaus/ineffassign/blob/4cc7213b9bc8b868b2990c372f6fa057fa88b91c/ineffassign.go#L70
ok_retcodes = [0, 3],
).wait()
# ineffassign emits some duplicate lines.
for line in sorted(set(res.stderr.splitlines())):
match = ctx.re.match(r"^%s/(.+):(\d+):(\d+): (.+)$" % ctx.scm.root, line)
ctx.emit.finding(
level = "error",
filepath = match.groups[1],
line = int(match.groups[2]),
col = int(match.groups[3]),
message = match.groups[4],
)
ineffassign = shac.check(_ineffassign)
def _staticcheck(ctx, version = "v0.4.3"):
"""Runs staticcheck on a Go code base.
See https://github.com/dominikh/go-tools for more details.
Args:
ctx: A ctx instance.
version: staticcheck version to install. Defaults to a recent version, that
will be rolled from time to time.
"""
exe = go_install(ctx, "honnef.co/go/tools/cmd/staticcheck", version)
env = go_env(ctx, "staticcheck")
env["STATICCHECK_CACHE"] = env["GOCACHE"]
res = ctx.os.exec(
[exe, "-f=json", "./..."],
ok_retcodes = [0, 1],
env = env,
).wait()
# Output is JSON-lines.
# https://staticcheck.io/docs/running-staticcheck/cli/formatters/#json
for line in res.stdout.splitlines():
f = json.decode(line)
end_kwargs = {}
if f["end"]["line"]:
end_kwargs["end_line"] = f["end"]["line"]
end_kwargs["end_col"] = f["end"]["column"]
ctx.emit.finding(
# Either "error" or "warning".
level = f["severity"],
message = f["message"],
# TODO(olivernewman): Add a relpath method for converting abs paths to
# relative.
filepath = f["location"]["file"][len(ctx.scm.root) + 1:],
line = f["location"]["line"],
col = f["location"]["column"],
**end_kwargs
)
staticcheck = shac.check(_staticcheck)
def _shadow(ctx, version = "v0.7.0"):
"""Runs go vet -vettool=shadow on a Go code base.
Args:
ctx: A ctx instance.
version: shadow version to install. Defaults to a recent version, that will
be rolled from time to time.
"""
exe = go_install(ctx, "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow", version)
res = ctx.os.exec(
[
exe,
# TODO(olivernewman): For some reason, including tests results in
# duplicate findings in non-test files.
"-test=false",
"-json",
"./...",
],
env = go_env(ctx, "shadow"),
).wait()
# Example output:
# {
# "github.com/foo/bar": {
# "shadow": [
# {
# "posn": "/abs/path/to/project/file.go:123:8",
# "message": "declaration of \"err\" shadows declaration at line 123"
# }
# ]
# }
# }
output = json.decode(res.stdout)
findings = []
for pkg_findings in output.values():
findings.extend(pkg_findings["shadow"])
for finding in findings:
match = ctx.re.match(r"^%s/(.+):(\d+):(\d+)$" % ctx.scm.root, finding["posn"])
ctx.emit.finding(
level = "error",
filepath = match.groups[1],
line = int(match.groups[2]),
col = int(match.groups[3]),
message = finding["message"],
)
shadow = shac.check(_shadow)
def no_fork_without_lock(ctx):
"""Checks that exec.Command Start() and Run() aren't called directly.
Instead, callers should use the `execsupport` package, which provides appropriate
locks to make sure forks are safe.
Args:
ctx: A ctx instance.
"""
output = json.decode(ctx.os.exec(
[
"go",
"run",
"./internal/go_checks/fork_check",
"-test=false",
"-json",
"./...",
],
env = go_env(ctx, "no_fork_without_lock"),
).wait().stdout)
# Skip the "execsupport" package since it contains the wrappers around Run()
# and Start() that should be used. But if it's not present in the output,
# that's a
# good sign that the check is broken.
if not output.pop("go.fuchsia.dev/shac-project/shac/internal/execsupport", None):
fail("execsupport package was not found in the output, fork_check may be buggy")
for checks in output.values():
for findings in checks.values():
for finding in findings:
match = ctx.re.match(r"^%s/(.+):(\d+):(\d+)$" % ctx.scm.root, finding["posn"])
ctx.emit.finding(
level = "error",
filepath = match.groups[1],
line = int(match.groups[2]),
col = int(match.groups[3]),
message = finding["message"],
)