mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-27 12:50:09 +00:00
150 lines
3.9 KiB
Python
150 lines
3.9 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
import os
|
|
import json
|
|
from json.decoder import JSONDecodeError
|
|
|
|
import mozpack.path as mozpath
|
|
from mozfile import which
|
|
from mozpack.files import FileFinder
|
|
|
|
from mozlint import result
|
|
from mozlint.util.implementation import LintProcess
|
|
|
|
SHELLCHECK_NOT_FOUND = """
|
|
Unable to locate shellcheck, please ensure it is installed and in
|
|
your PATH or set the SHELLCHECK environment variable.
|
|
|
|
https://shellcheck.net or your system's package manager.
|
|
""".strip()
|
|
|
|
results = []
|
|
|
|
|
|
class ShellcheckProcess(LintProcess):
|
|
def process_line(self, line):
|
|
try:
|
|
data = json.loads(line)
|
|
except JSONDecodeError as e:
|
|
print("Unable to load shellcheck output ({}): {}".format(e, line))
|
|
return
|
|
|
|
for entry in data:
|
|
res = {
|
|
"path": entry["file"],
|
|
"message": entry["message"],
|
|
"level": "error",
|
|
"lineno": entry["line"],
|
|
"column": entry["column"],
|
|
"rule": entry["code"],
|
|
}
|
|
results.append(result.from_config(self.config, **res))
|
|
|
|
|
|
def determine_shell_from_script(path):
|
|
"""Returns a string identifying the shell used.
|
|
|
|
Returns None if not identifiable.
|
|
|
|
Copes with the following styles:
|
|
#!bash
|
|
#!/bin/bash
|
|
#!/usr/bin/env bash
|
|
"""
|
|
with open(path, "r") as f:
|
|
head = f.readline()
|
|
|
|
if not head.startswith("#!"):
|
|
return
|
|
|
|
# allow for parameters to the shell
|
|
shebang = head.split()[0]
|
|
|
|
# if the first entry is a variant of /usr/bin/env
|
|
if "env" in shebang:
|
|
shebang = head.split()[1]
|
|
|
|
if shebang.endswith("sh"):
|
|
# Strip first to avoid issues with #!bash
|
|
return shebang.strip("#!").split("/")[-1]
|
|
# make it clear we return None, rather than fall through.
|
|
return
|
|
|
|
|
|
def find_shell_scripts(config, paths):
|
|
found = dict()
|
|
|
|
root = config["root"]
|
|
exclude = [mozpath.join(root, e) for e in config.get("exclude", [])]
|
|
|
|
if config.get("extensions"):
|
|
pattern = "**/*.{}".format(config.get("extensions")[0])
|
|
else:
|
|
pattern = "**/*.sh"
|
|
|
|
files = []
|
|
for path in paths:
|
|
path = mozpath.normsep(path)
|
|
ignore = [
|
|
e[len(path) :].lstrip("/")
|
|
for e in exclude
|
|
if mozpath.commonprefix((path, e)) == path
|
|
]
|
|
finder = FileFinder(path, ignore=ignore)
|
|
files.extend([os.path.join(path, p) for p, f in finder.find(pattern)])
|
|
|
|
for filename in files:
|
|
shell = determine_shell_from_script(filename)
|
|
if shell:
|
|
found[filename] = shell
|
|
return found
|
|
|
|
|
|
def run_process(config, cmd):
|
|
proc = ShellcheckProcess(config, cmd)
|
|
proc.run()
|
|
try:
|
|
proc.wait()
|
|
except KeyboardInterrupt:
|
|
proc.kill()
|
|
|
|
|
|
def get_shellcheck_binary():
|
|
"""
|
|
Returns the path of the first shellcheck binary available
|
|
if not found returns None
|
|
"""
|
|
binary = os.environ.get("SHELLCHECK")
|
|
if binary:
|
|
return binary
|
|
|
|
return which("shellcheck")
|
|
|
|
|
|
def lint(paths, config, **lintargs):
|
|
log = lintargs["log"]
|
|
binary = get_shellcheck_binary()
|
|
|
|
if not binary:
|
|
print(SHELLCHECK_NOT_FOUND)
|
|
if "MOZ_AUTOMATION" in os.environ:
|
|
return 1
|
|
return []
|
|
|
|
config["root"] = lintargs["root"]
|
|
|
|
files = find_shell_scripts(config, paths)
|
|
|
|
base_command = [binary, "-f", "json"]
|
|
if config.get("excludecodes"):
|
|
base_command.extend(["-e", ",".join(config.get("excludecodes"))])
|
|
|
|
for f in files:
|
|
cmd = list(base_command)
|
|
cmd.extend(["-s", files[f], f])
|
|
log.debug("Command: {}".format(cmd))
|
|
run_process(config, cmd)
|
|
return results
|