Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shellcheck linter #692

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bioconda_utils/bioconda_utils-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ anaconda-client=1.6.* # anaconda_upload
involucro=1.1.* # mulled test and container build
skopeo=0.1.35 # docker upload
git=2.14.* # well - git
shellcheck=0.7.* # shellcheck linter

# hosters - special regex not supported by RE
regex=2018.08.29
Expand Down
1 change: 1 addition & 0 deletions bioconda_utils/lint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
check_noarch
check_policy
check_repo
check_shellcheck
check_syntax

"""
Expand Down
73 changes: 73 additions & 0 deletions bioconda_utils/lint/check_shellcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import json
import os
from subprocess import run
import warnings

from . import LintCheck, LintMessage, ERROR, WARNING, INFO

class shellcheck(LintCheck):
"""The recipe uses shell scripts with portability/reliability issues

The shell scripts should be checked and either fixed or annotated with
# shellcheck disable=<error-code>
tags
"""

_SEVERITY_MAP = {
"error": ERROR,
"warning": WARNING,
"info": INFO,
"style": None
}

def _run_shellcheck(self, recipe):
sh_files = [x for x in os.listdir(recipe.dir) if x.ends_with(".sh") and os.path.isfile(x)]
if not sh_files:
return None
try:
res = run(["shellcheck", "-f", "json1"] + sh_files, capture_output = True, check = False)
except FileNotFoundError:
warnings.warn("Skipping shellcheck linter because it could not be found")
return None
if res.returncode == 0:
return None
try:
errors = json.loads(res.stdout)
except json.JSONDecodeError:
print("Skipping shellcheck it returned unexpected output: " + res.stdout)
return None
return errors["comments"]

def fix(self, error):
# shellcheck may provide in error["fix"] suggestions to fix the error, but
# we don't implement them here. The error message is usually helpful enough
return False

@classmethod
def _shellcheck_msg(cls, recipe, error):
error_severity = cls._SEVERITY_MAP[error["level"]]
if error_severity is None:
return None
return LintMessage(
recipe=recipe,
check=cls,
severity=error_severity,
title="shellcheck",
body=error["message"],
fname=error["file"],
start_line=error["line"],
end_line=error["endLine"],
canfix=False
)

def check_recipe(self, recipe):
errors = self._run_shellcheck(recipe)
if errors is None:
return
for error in errors:
if self.try_fix and error.get("fix", None):
if self.fix(error):
continue
msg = self._shellcheck_msg(recipe, error)
if msg is not None:
self.messages.append(msg)