lint: add shebang checker
BUG=None
TEST=`./cbuildbot/run_tests` passes
Change-Id: Ic46eaf13f7fe910938353baeaa6301002c6c68b3
Reviewed-on: https://chromium-review.googlesource.com/243773
Reviewed-by: Anatol Pomazau <anatol@google.com>
Trybot-Ready: Mike Frysinger <vapier@chromium.org>
Tested-by: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Don Garrett <dgarrett@chromium.org>
Commit-Queue: Mike Frysinger <vapier@chromium.org>
diff --git a/cros/commands/lint.py b/cros/commands/lint.py
index d6adc0f..e4dfe89 100644
--- a/cros/commands/lint.py
+++ b/cros/commands/lint.py
@@ -26,6 +26,9 @@
from pylint.interfaces import IAstroidChecker
+# pylint: disable=too-few-public-methods
+
+
class DocStringChecker(BaseChecker):
"""PyLint AST based checker to verify PEP 257 compliance
@@ -38,8 +41,7 @@
__implements__ = IAstroidChecker
- # pylint: disable=too-few-public-methods,multiple-statements
- # pylint: disable=class-missing-docstring
+ # pylint: disable=class-missing-docstring,multiple-statements
class _MessageCP001(object): pass
class _MessageCP002(object): pass
class _MessageCP003(object): pass
@@ -54,8 +56,7 @@
class _MessageCP012(object): pass
class _MessageCP013(object): pass
class _MessageCP014(object): pass
- # pylint: enable=too-few-public-methods,multiple-statements
- # pylint: enable=class-missing-docstring
+ # pylint: enable=class-missing-docstring,multiple-statements
name = 'doc_string_checker'
priority = -1
@@ -308,11 +309,9 @@
__implements__ = IAstroidChecker
- # pylint: disable=too-few-public-methods,multiple-statements
- # pylint: disable=class-missing-docstring
+ # pylint: disable=class-missing-docstring,multiple-statements
class _MessageR9100(object): pass
- # pylint: enable=too-few-public-methods,multiple-statements
- # pylint: enable=class-missing-docstring
+ # pylint: enable=class-missing-docstring,multiple-statements
name = 'py3k_compat_checker'
priority = -1
@@ -353,6 +352,55 @@
self.saw_imports = True
+class SourceChecker(BaseChecker):
+ """Make sure we enforce py3k compatible features"""
+
+ __implements__ = IAstroidChecker
+
+ # pylint: disable=class-missing-docstring,multiple-statements
+ class _MessageR9200(object): pass
+ class _MessageR9201(object): pass
+ class _MessageR9202(object): pass
+ # pylint: enable=class-missing-docstring,multiple-statements
+
+ name = 'source_checker'
+ priority = -1
+ MSG_ARGS = 'offset:%(offset)i: {%(line)s}'
+ msgs = {
+ 'R9200': ('Shebang should be #!/usr/bin/python2 or #!/usr/bin/python3',
+ ('bad-shebang'), _MessageR9200),
+ 'R9201': ('Shebang is missing, but file is executable',
+ ('missing-shebang'), _MessageR9201),
+ 'R9202': ('Shebang is set, but file is not executable',
+ ('spurious-shebang'), _MessageR9202),
+ }
+ options = ()
+
+ def visit_module(self, node):
+ """Called when the whole file has been read"""
+ stream = node.file_stream
+ stream.seek(0)
+ self._check_shebang(node, stream)
+
+ def _check_shebang(self, _node, stream):
+ """Verify the shebang is version specific"""
+ st = os.fstat(stream.fileno())
+ mode = st.st_mode
+ executable = bool(mode & 0o0111)
+
+ shebang = stream.readline()
+ if shebang[0:2] != '#!':
+ if executable:
+ self.add_message('R9201')
+ return
+ elif not executable:
+ self.add_message('R9202')
+
+ parts = shebang.split()
+ if parts[0] not in ('#!/usr/bin/python2', '#!/usr/bin/python3'):
+ self.add_message('R9200')
+
+
def register(linter):
"""pylint will call this func to register all our checkers"""
# Walk all the classes in this module and register ours.
diff --git a/cros/commands/lint_unittest.py b/cros/commands/lint_unittest.py
index 9e26a9d..5f7866a 100644
--- a/cros/commands/lint_unittest.py
+++ b/cros/commands/lint_unittest.py
@@ -7,9 +7,13 @@
from __future__ import print_function
import collections
+import StringIO
from chromite.lib import cros_test_lib
-import lint
+from chromite.cros.commands import lint
+
+
+# pylint: disable=protected-access
class TestNode(object):
@@ -31,7 +35,25 @@
return self.args
-class DocStringCheckerTest(cros_test_lib.TestCase):
+class CheckerTestCase(cros_test_lib.TestCase):
+ """Helpers for Checker modules"""
+
+ def add_message(self, msg_id, node=None, line=None, args=None):
+ """Capture lint checks"""
+ # We include node.doc here explicitly so the pretty assert message
+ # inclues it in the output automatically.
+ doc = node.doc if node else ''
+ self.results.append((msg_id, doc, line, args))
+
+ def setUp(self):
+ assert hasattr(self, 'CHECKER'), 'TestCase must set CHECKER'
+
+ self.results = []
+ self.checker = self.CHECKER()
+ self.checker.add_message = self.add_message
+
+
+class DocStringCheckerTest(CheckerTestCase):
"""Tests for DocStringChecker module"""
GOOD_FUNC_DOCSTRINGS = (
@@ -141,16 +163,7 @@
""",
)
- def add_message(self, msg_id, node=None, line=None, args=None):
- """Capture lint checks"""
- # We include node.doc here explicitly so the pretty assert message
- # inclues it in the output automatically.
- self.results.append((msg_id, node.doc, line, args))
-
- def setUp(self):
- self.results = []
- self.checker = lint.DocStringChecker()
- self.checker.add_message = self.add_message
+ CHECKER = lint.DocStringChecker
def testGood_visit_function(self):
"""Allow known good docstrings"""
@@ -183,7 +196,6 @@
def testGood_check_first_line(self):
"""Verify _check_first_line accepts good inputs"""
- # pylint: disable=W0212
docstrings = (
'Some string',
)
@@ -196,7 +208,6 @@
def testBad_check_first_line(self):
"""Verify _check_first_line rejects bad inputs"""
- # pylint: disable=W0212
docstrings = (
'\nSome string\n',
)
@@ -208,7 +219,6 @@
def testGood_check_second_line_blank(self):
"""Verify _check_second_line_blank accepts good inputs"""
- # pylint: disable=protected-access
docstrings = (
'Some string\n\nThis is the third line',
'Some string',
@@ -222,7 +232,6 @@
def testBad_check_second_line_blank(self):
"""Verify _check_second_line_blank rejects bad inputs"""
- # pylint: disable=protected-access
docstrings = (
'Some string\nnonempty secondline',
)
@@ -234,7 +243,6 @@
def testGoodFuncVarKwArg(self):
"""Check valid inputs for *args and **kwargs"""
- # pylint: disable=W0212
for vararg in (None, 'args', '_args'):
for kwarg in (None, 'kwargs', '_kwargs'):
self.results = []
@@ -244,7 +252,6 @@
def testMisnamedFuncVarKwArg(self):
"""Reject anything but *args and **kwargs"""
- # pylint: disable=W0212
for vararg in ('arg', 'params', 'kwargs', '_moo'):
self.results = []
node = TestNode(vararg=vararg)
@@ -259,7 +266,6 @@
def testGoodFuncArgs(self):
"""Verify normal args in Args are allowed"""
- # pylint: disable=W0212
datasets = (
("""args are correct, and cls is ignored
@@ -295,7 +301,6 @@
def testBadFuncArgs(self):
"""Verify bad/missing args in Args are caught"""
- # pylint: disable=W0212
datasets = (
("""missing 'bar'
@@ -331,3 +336,41 @@
node = TestNode(doc=dc, args=args)
self.checker._check_all_args_in_doc(node, node.lines)
self.assertEqual(len(self.results), 1)
+
+
+class SourceCheckerTest(CheckerTestCase):
+ """Tests for SourceChecker module"""
+
+ CHECKER = lint.SourceChecker
+
+ def _testShebang(self, shebangs, exp, fileno):
+ """Helper for shebang tests"""
+ for shebang in shebangs:
+ self.results = []
+ node = TestNode()
+ stream = StringIO.StringIO(shebang)
+ stream.fileno = lambda: fileno
+ self.checker._check_shebang(node, stream)
+ self.assertEqual(len(self.results), exp,
+ msg='processing shebang failed: %r' % shebang)
+
+ def testBadShebangNoExec(self):
+ """Verify _check_shebang rejects bad shebangs"""
+ shebangs = (
+ '#!/usr/bin/python\n',
+ '#! /usr/bin/python2 \n',
+ '#!/usr/bin/env python3\n',
+ )
+ with open('/dev/null') as f:
+ self._testShebang(shebangs, 2, f.fileno())
+
+ def testGoodShebang(self):
+ """Verify _check_shebang accepts good shebangs"""
+ shebangs = (
+ '#!/usr/bin/python2\n',
+ '#!/usr/bin/python2 \n',
+ '#!/usr/bin/python3\n',
+ '#!/usr/bin/python3\t\n',
+ )
+ with open('/bin/sh') as f:
+ self._testShebang(shebangs, 0, f.fileno())