zulip/tools/lint-all

293 lines
8.9 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env python2.7
from __future__ import print_function
from __future__ import absolute_import
import os
import re
import sys
import optparse
import subprocess
import traceback
from os import path
from collections import defaultdict
from six.moves import filter
from six.moves import map
import lister
parser = optparse.OptionParser()
parser.add_option('--full',
action='store_true',
help='Check some things we typically ignore')
parser.add_option('--modified', '-m',
action='store_true',
help='Only check modified files')
parser.add_option('--verbose', '-v',
action='store_true',
help='Print verbose timing output')
(options, args) = parser.parse_args()
os.chdir(path.join(path.dirname(__file__), '..'))
# Exclude some directories and files from lint checking
exclude = """
static/third
confirmation
frontend_tests/casperjs
zerver/migrations
node_modules
docs/html_unescape.py
zproject/test_settings.py
zproject/settings.py
tools/jslint/jslint.js
api/setup.py
api/integrations/perforce/git_p4.py
puppet/apt/.forge-release
puppet/puppet-common/tests/
puppet/apt/README.md
""".split()
by_lang = lister.list_files(args, modified_only=options.modified, use_shebang=True,
ftypes=['py', 'sh', 'js', 'pp', 'css', 'handlebars', 'html', 'json', 'md', 'txt', 'text'],
group_by_ftype=True, exclude=exclude)
# Invoke the appropriate lint checker for each language,
# and also check files for extra whitespace.
import logging
logging.basicConfig(format="%(asctime)s %(message)s")
logger = logging.getLogger()
if options.verbose:
logger.setLevel(logging.INFO)
else:
logger.setLevel(logging.WARNING)
def check_pyflakes():
if not by_lang['py']:
return False
failed = False
pyflakes = subprocess.Popen(['pyflakes'] + by_lang['py'],
stdout = subprocess.PIPE,
stderr = subprocess.PIPE)
# pyflakes writes some output (like syntax errors) to stderr. :/
for pipe in (pyflakes.stdout, pyflakes.stderr):
for ln in pipe:
if options.full or not \
('imported but unused' in ln or
'redefinition of unused' in ln or
("zerver/models.py" in ln and
" undefined name 'bugdown'" in ln) or
("scripts/lib/pythonrc.py" in ln and
" import *' used; unable to detect undefined names" in ln) or
("zerver/lib/tornado_ioloop_logging.py" in ln and
"redefinition of function 'instrument_tornado_ioloop'" in ln) or
("zephyr_mirror_backend.py:" in ln and
"redefinition of unused 'simplejson' from line" in ln)):
sys.stdout.write(ln)
failed = True
return failed
def custom_check_file(fn, rules, skip_rules=[]):
failed = False
lineFlag = False
for i, line in enumerate(open(fn)):
skip = False
lineFlag = True
for rule in skip_rules:
if re.match(rule, line):
skip = True
if skip:
continue
for rule in rules:
exclude_list = rule.get('exclude', set())
if fn in exclude_list:
continue
try:
if re.search(rule['pattern'], line.strip(rule.get('strip', None))):
sys.stdout.write(rule['description'] + ' at %s line %s:\n' % (fn, i+1))
print(line)
failed = True
except Exception:
print("Exception with %s at %s line %s" % (rule['pattern'], fn, i+1))
traceback.print_exc()
lastLine = line
if lineFlag and '\n' not in lastLine:
print("No newline at the end of file. Fix with `sed -i '$a\\' %s`" % (fn,))
failed = True
return failed
whitespace_rules = [
# This linter should be first since bash_rules depends on it.
{'pattern': '\s+$',
'strip': '\n',
'description': 'Fix trailing whitespace'},
{'pattern': '\t',
'strip': '\n',
'exclude': set(['zerver/lib/bugdown/codehilite.py']),
'description': 'Fix tab-based whitespace'},
]
js_rules = [
{'pattern': '[^_]function\(',
'description': 'The keyword "function" should be followed by a space'},
{'pattern': '.*blueslip.warning\(.*',
'description': 'The module blueslip has no function warning, try using blueslip.warn'},
{'pattern': '[)]{$',
'description': 'Missing space between ) and {'},
# This rule is constructed with + to avoid triggering on itself
{'pattern': " =" + '[^ =>~"]',
'description': 'Missing whitespace after "="'},
{'pattern': 'else{$',
'description': 'Missing space between else and {'},
] + whitespace_rules
python_rules = [
{'pattern': '^(?!#)@login_required',
'description': '@login_required is unsupported; use @zulip_login_required'},
{'pattern': '".*"%\([a-z_].*\)?$',
'description': 'Missing space around "%"'},
{'pattern': "'.*'%\([a-z_].*\)?$",
'description': 'Missing space around "%"'},
# This rule is constructed with + to avoid triggering on itself
{'pattern': " =" + '[^ =>~"]',
'description': 'Missing whitespace after "="'},
{'pattern': '":\w[^"]*$',
'description': 'Missing whitespace after ":"'},
{'pattern': "':\w[^']*$",
'description': 'Missing whitespace after ":"'},
{'pattern': "^\s+[#]\w",
'strip': '\n',
'description': 'Missing whitespace after "#"'},
{'pattern': ", [)]",
'description': 'Unnecessary whitespace between "," and ")"'},
{'pattern': "% [(]",
'description': 'Unnecessary whitespace between "%" and "("'},
# This next check could have false positives, but it seems pretty
# rare; if we find any, they can be added to the exclude list for
# this rule.
{'pattern': '% [a-zA-Z0-9_.]*\)?$',
'description': 'Used % comprehension without a tuple'},
] + whitespace_rules
bash_rules = [
{'pattern': '#!.*sh [-xe]',
'description': 'Fix shebang line with proper call to /usr/bin/env for Bash path, change -x|-e switches to set -x|set -e'},
] + whitespace_rules[0:1]
css_rules = [
{'pattern': '^[^:]*:\S[^:]*;$',
'description': "Missing whitespace after : in CSS"},
{'pattern': '{\w',
'description': "Missing whitespace after '{' in CSS (should be newline)."},
] + whitespace_rules
handlebars_rules = whitespace_rules
2016-04-08 20:44:39 +02:00
html_rules = whitespace_rules
json_rules = [] # just fix newlines at ends of files
markdown_rules = whitespace_rules
txt_rules = whitespace_rules
def check_custom_checks():
failed = False
for fn in by_lang['py']:
if custom_check_file(fn, python_rules):
failed = True
for fn in by_lang['js']:
if custom_check_file(fn, js_rules):
failed = True
for fn in by_lang['sh']:
if custom_check_file(fn, bash_rules):
failed = True
2016-04-08 20:38:25 +02:00
for fn in by_lang['css']:
if custom_check_file(fn, css_rules):
failed = True
for fn in by_lang['handlebars']:
if custom_check_file(fn, handlebars_rules):
failed = True
2016-04-08 20:44:39 +02:00
for fn in by_lang['html']:
if custom_check_file(fn, html_rules):
failed = True
for fn in by_lang['json']:
if custom_check_file(fn, json_rules):
failed = True
for fn in by_lang['md']:
if custom_check_file(fn, markdown_rules):
failed = True
for fn in by_lang['txt'] + by_lang['text']:
if custom_check_file(fn, txt_rules):
failed = True
return failed
lint_functions = {}
def run_parallel():
pids = []
for name, func in lint_functions.items():
pid = os.fork()
if pid == 0:
logging.info("start " + name)
result = func()
logging.info("finish " + name)
os._exit(result)
pids.append(pid)
failed = False
for pid in pids:
(_, status) = os.waitpid(pid, 0)
if status != 0:
failed = True
return failed
def lint(func):
lint_functions[func.__name__] = func
return func
try:
# Make the lint output bright red
sys.stdout.write('\x1B[1;31m')
sys.stdout.flush()
@lint
def templates():
result = subprocess.call(['tools/check-templates'])
return result
@lint
def jslint():
result = subprocess.call(['tools/node', 'tools/jslint/check-all.js']
+ by_lang['js'])
return result
@lint
def puppet():
if not by_lang['pp']:
return 0
result = subprocess.call(['puppet', 'parser', 'validate'] + by_lang['pp'])
return result
@lint
def custom():
failed = check_custom_checks()
return 1 if failed else 0
@lint
def pyflakes():
failed = check_pyflakes()
return 1 if failed else 0
failed = run_parallel()
sys.exit(1 if failed else 0)
finally:
# Restore normal terminal colors
sys.stdout.write('\x1B[0m')