[javascriptlint-commit] SF.net SVN: javascriptlint:[255] trunk
Status: Beta
Brought to you by:
matthiasmiller
From: <mat...@us...> - 2009-10-03 16:53:15
|
Revision: 255 http://javascriptlint.svn.sourceforge.net/javascriptlint/?rev=255&view=rev Author: matthiasmiller Date: 2009-10-03 16:53:02 +0000 (Sat, 03 Oct 2009) Log Message: ----------- Move the contents of pyjsl out one level. Modified Paths: -------------- trunk/javascriptlint/jsl.py trunk/test.py Added Paths: ----------- trunk/javascriptlint/conf.py trunk/javascriptlint/htmlparse.py trunk/javascriptlint/jsparse.py trunk/javascriptlint/lint.py trunk/javascriptlint/spidermonkey.py trunk/javascriptlint/spidermonkey_.py trunk/javascriptlint/util.py trunk/javascriptlint/visitation.py trunk/javascriptlint/warnings.py Removed Paths: ------------- trunk/javascriptlint/pyjsl/ Copied: trunk/javascriptlint/conf.py (from rev 254, trunk/javascriptlint/pyjsl/conf.py) =================================================================== --- trunk/javascriptlint/conf.py (rev 0) +++ trunk/javascriptlint/conf.py 2009-10-03 16:53:02 UTC (rev 255) @@ -0,0 +1,136 @@ +# vim: ts=4 sw=4 expandtab +import os + +import warnings + +class ConfError(Exception): + def __init__(self, error): + Exception.__init__(self, error) + self.lineno = None + self.path = None + +class Setting: + wants_parm = False + wants_dir = False + +class BooleanSetting(Setting): + wants_parm = False + def __init__(self, default): + self.value = default + def load(self, enabled): + self.value = enabled + +class StringSetting(Setting): + wants_parm = True + def __init__(self, default): + self.value = default + def load(self, enabled, parm): + if not enabled: + raise ConfError, 'Expected +.' + self.value = parm + +class DeclareSetting(Setting): + wants_parm = True + def __init__(self): + self.value = [] + def load(self, enabled, parm): + if not enabled: + raise ConfError, 'Expected +.' + self.value.append(parm) + +class ProcessSetting(Setting): + wants_parm = True + wants_dir = True + def __init__(self, recurse_setting): + self.value = [] + self._recurse = recurse_setting + def load(self, enabled, parm, dir): + if dir: + parm = os.path.join(dir, parm) + self.value.append((self._recurse.value, parm)) + +class Conf: + def __init__(self): + recurse = BooleanSetting(False) + self._settings = { + 'recurse': recurse, + 'show_context': BooleanSetting(False), + 'output-format': StringSetting('__FILE__(__LINE__): __ERROR__'), + 'lambda_assign_requires_semicolon': BooleanSetting(False), + 'legacy_control_comments': BooleanSetting(True), + 'jscript_function_extensions': BooleanSetting(False), + 'always_use_option_explicit': BooleanSetting(False), + 'define': DeclareSetting(), + 'context': BooleanSetting(False), + 'process': ProcessSetting(recurse), + # SpiderMonkey warnings + 'no_return_value': BooleanSetting(True), + 'equal_as_assign': BooleanSetting(True), + 'anon_no_return_value': BooleanSetting(True) + } + for name in warnings.warnings: + self._settings[name] = BooleanSetting(True) + self.loadline('-block_without_braces') + + def loadfile(self, path): + path = os.path.abspath(path) + conf = open(path, 'r').read() + try: + self.loadtext(conf, dir=os.path.dirname(path)) + except ConfError, error: + error.path = path + raise + + def loadtext(self, conf, dir=None): + lines = conf.splitlines() + for lineno in range(0, len(lines)): + try: + self.loadline(lines[lineno], dir) + except ConfError, error: + error.lineno = lineno + raise + + def loadline(self, line, dir=None): + assert not '\r' in line + assert not '\n' in line + + # Allow comments + if '#' in line: + line = line[:line.find('#')] + line = line.rstrip() + if not line: + return + + # Parse the +/- + if line.startswith('+'): + enabled = True + elif line.startswith('-'): + enabled = False + else: + raise ConfError, 'Expected + or -.' + line = line[1:] + + # Parse the key/parms + name = line.split()[0].lower() + parm = line[len(name):].lstrip() + + # Load the setting + setting = self._settings[name] + args = { + 'enabled': enabled + } + if setting.wants_parm: + args['parm'] = parm + elif parm: + raise ConfError, 'The %s setting does not expect a parameter.' % name + if setting.wants_dir: + args['dir'] = dir + setting.load(**args) + + def __getitem__(self, name): + if name == 'paths': + name = 'process' + elif name == 'declarations': + name = 'define' + return self._settings[name].value + Property changes on: trunk/javascriptlint/conf.py ___________________________________________________________________ Added: svn:mergeinfo + Added: svn:eol-style + native Copied: trunk/javascriptlint/htmlparse.py (from rev 254, trunk/javascriptlint/pyjsl/htmlparse.py) =================================================================== --- trunk/javascriptlint/htmlparse.py (rev 0) +++ trunk/javascriptlint/htmlparse.py 2009-10-03 16:53:02 UTC (rev 255) @@ -0,0 +1,89 @@ +# vim: ts=4 sw=4 expandtab +import HTMLParser +import unittest + +import jsparse + +class _Parser(HTMLParser.HTMLParser): + def __init__(self): + HTMLParser.HTMLParser.__init__(self) + self._node_positions = jsparse.NodePositions('') + self._script = None + self._scripts = [] + + def handle_starttag(self, tag, attributes): + if tag.lower() == 'script' and not self._script: + offset = self._getoffset() + len(self.get_starttag_text()) + self._script = self._script or { + 'start': offset, + 'attributes': attributes + } + + def handle_endtag(self, tag): + if tag.lower() == 'script' and self._script: + start = self._script['start'] + end = self._getoffset() + script = self.rawdata[start:end] + if jsparse.is_compilable_unit(script): + attr = dict(self._script['attributes']) + self._scripts.append({ + 'script': script, + 'startoffset': start, + 'endoffset': end, + 'startpos': self._node_positions.from_offset(start), + 'endpos': self._node_positions.from_offset(end), + 'src': attr.get('src'), + 'type': attr.get('type') + }) + self._script = None + + def feed(self, data): + self._node_positions = jsparse.NodePositions(self.rawdata + data) + HTMLParser.HTMLParser.feed(self, data) + + def unknown_decl(self, data): + # Ignore unknown declarations instead of raising an exception. + pass + + def getscripts(self): + return self._scripts + + def _getnodepos(self): + line, col = self.getpos() + return jsparse.NodePos(line - 1, col) + + def _getoffset(self): + return self._node_positions.to_offset(self._getnodepos()) + +def findscripts(s): + parser = _Parser() + parser.feed(s) + parser.close() + return parser.getscripts() + +class TestHTMLParse(unittest.TestCase): + def testFindScript(self): + html = """ +<html><body> +<script src=test.js></script> +hi&b +a<script><!-- +var s = '<script></script>'; +--></script> +ok& +..</script> +ok& +</body> +</html> +""" + scripts = [x['script'] for x in findscripts(html)] + self.assertEquals(scripts, [ + "", + "<!--\nvar s = '<script></script>';\n-->" + ]) + def testConditionalComments(self): + html = """ +<!--[if IE]>This is Internet Explorer.<![endif]--> +<![if !IE]>This is not Internet Explorer<![endif]> +""" + findscripts(html) Property changes on: trunk/javascriptlint/htmlparse.py ___________________________________________________________________ Added: svn:mergeinfo + Added: svn:eol-style + native Modified: trunk/javascriptlint/jsl.py =================================================================== --- trunk/javascriptlint/jsl.py 2009-10-03 16:41:05 UTC (rev 254) +++ trunk/javascriptlint/jsl.py 2009-10-03 16:53:02 UTC (rev 255) @@ -7,11 +7,11 @@ import unittest from optparse import OptionParser -import pyjsl.conf -import pyjsl.htmlparse -import pyjsl.jsparse -import pyjsl.lint -import pyjsl.util +import conf +import htmlparse +import jsparse +import lint +import util _lint_results = { 'warnings': 0, @@ -20,15 +20,15 @@ def _dump(paths): for path in paths: - script = pyjsl.util.readfile(path) - pyjsl.jsparse.dump_tree(script) + script = util.readfile(path) + jsparse.dump_tree(script) -def _lint(paths, conf): +def _lint(paths, conf_): def lint_error(path, line, col, errname, errdesc): _lint_results['warnings'] = _lint_results['warnings'] + 1 - print pyjsl.util.format_error(conf['output-format'], path, line, col, + print util.format_error(conf_['output-format'], path, line, col, errname, errdesc) - pyjsl.lint.lint_files(paths, lint_error, conf=conf) + lint.lint_files(paths, lint_error, conf=conf_) def _resolve_paths(path, recurse): if os.path.isfile(path): @@ -88,9 +88,9 @@ parser.print_help() sys.exit() - conf = pyjsl.conf.Conf() + conf_ = conf.Conf() if options.conf: - conf.loadfile(options.conf) + conf_.loadfile(options.conf) profile_func = _profile_disabled if options.profile: @@ -98,21 +98,21 @@ if options.unittest: suite = unittest.TestSuite(); - for module in [pyjsl.htmlparse, pyjsl.jsparse, pyjsl.util]: + for module in [htmlparse, jsparse, util]: suite.addTest(unittest.findTestCases(module)) runner = unittest.TextTestRunner(verbosity=options.verbosity) runner.run(suite) paths = [] - for recurse, path in conf['paths']: + for recurse, path in conf_['paths']: paths.extend(_resolve_paths(path, recurse)) for arg in args: paths.extend(_resolve_paths(arg, False)) if options.dump: profile_func(_dump, paths) else: - profile_func(_lint, paths, conf) + profile_func(_lint, paths, conf_) if _lint_results['errors']: sys.exit(3) Copied: trunk/javascriptlint/jsparse.py (from rev 254, trunk/javascriptlint/pyjsl/jsparse.py) =================================================================== --- trunk/javascriptlint/jsparse.py (rev 0) +++ trunk/javascriptlint/jsparse.py 2009-10-03 16:53:02 UTC (rev 255) @@ -0,0 +1,414 @@ +#!/usr/bin/python +# vim: ts=4 sw=4 expandtab +""" Parses a script into nodes. """ +import bisect +import re +import unittest + +import spidermonkey +from spidermonkey import tok, op + +_tok_names = dict(zip( + [getattr(tok, prop) for prop in dir(tok)], + ['tok.%s' % prop for prop in dir(tok)] +)) +_op_names = dict(zip( + [getattr(op, prop) for prop in dir(op)], + ['op.%s' % prop for prop in dir(op)] +)) + +NodePos = spidermonkey.NodePos + +class NodePositions: + " Given a string, allows [x] lookups for NodePos line and column numbers." + def __init__(self, text, start_pos=None): + # Find the length of each line and incrementally sum all of the lengths + # to determine the ending position of each line. + self._start_pos = start_pos + self._lines = text.splitlines(True) + lines = [0] + [len(x) for x in self._lines] + for x in range(1, len(lines)): + lines[x] += lines[x-1] + self._line_offsets = lines + def from_offset(self, offset): + line = bisect.bisect(self._line_offsets, offset)-1 + col = offset - self._line_offsets[line] + if self._start_pos: + if line == 0: + col += self._start_pos.col + line += self._start_pos.line + return NodePos(line, col) + def to_offset(self, pos): + pos = self._to_rel_pos(pos) + offset = self._line_offsets[pos.line] + pos.col + assert offset <= self._line_offsets[pos.line+1] # out-of-bounds col num + return offset + def text(self, start, end): + assert start <= end + start, end = self._to_rel_pos(start), self._to_rel_pos(end) + # Trim the ending first in case it's a single line. + lines = self._lines[start.line:end.line+1] + lines[-1] = lines[-1][:end.col+1] + lines[0] = lines[0][start.col:] + return ''.join(lines) + def _to_rel_pos(self, pos): + " converts a position to a position relative to self._start_pos " + if not self._start_pos: + return pos + line, col = pos.line, pos.col + line -= self._start_pos.line + if line == 0: + col -= self._start_pos.col + assert line >= 0 and col >= 0 # out-of-bounds node position + return NodePos(line, col) + +class NodeRanges: + def __init__(self): + self._offsets = [] + def add(self, start, end): + i = bisect.bisect_left(self._offsets, start) + if i % 2 == 1: + i -= 1 + start = self._offsets[i] + + end = end + 1 + j = bisect.bisect_left(self._offsets, end) + if j % 2 == 1: + end = self._offsets[j] + j += 1 + + self._offsets[i:j] = [start,end] + def has(self, pos): + return bisect.bisect_right(self._offsets, pos) % 2 == 1 + +class _Node: + def add_child(self, node): + if node: + node.node_index = len(self.kids) + node.parent = self + self.kids.append(node) + + def start_pos(self): + try: + return self._start_pos + except AttributeError: + self._start_pos = NodePos(self._start_line, self._start_col) + return self._start_pos + + def end_pos(self): + try: + return self._end_pos + except AttributeError: + self._end_pos = NodePos(self._end_line, self._end_col) + return self._end_pos + + def __str__(self): + kind = self.kind + if not kind: + kind = '(none)' + return '%s>%s' % (_tok_names[kind], str(self.kids)) + + def is_equivalent(self, other, are_functions_equiv=False): + if not other: + return False + + # Bail out for functions + if not are_functions_equiv: + if self.kind == tok.FUNCTION: + return False + if self.kind == tok.LP and self.opcode == op.CALL: + return False + + if self.kind != other.kind: + return False + if self.opcode != other.opcode: + return False + + # Check atoms on names, properties, and string constants + if self.kind in (tok.NAME, tok.DOT, tok.STRING) and self.atom != other.atom: + return False + + # Check values on numbers + if self.kind == tok.NUMBER and self.dval != other.dval: + return False + + # Compare child nodes + if len(self.kids) != len(other.kids): + return False + for i in range(0, len(self.kids)): + # Watch for dead nodes + if not self.kids[i]: + if not other.kids[i]: return True + else: return False + if not self.kids[i].is_equivalent(other.kids[i]): + return False + + return True + +def findpossiblecomments(script, node_positions): + pos = 0 + single_line_re = r"//[^\r\n]*" + multi_line_re = r"/\*(.*?)\*/" + full_re = "(%s)|(%s)" % (single_line_re, multi_line_re) + comment_re = re.compile(full_re, re.DOTALL) + + comments = [] + while True: + match = comment_re.search(script, pos) + if not match: + return comments + + # Get the comment text + comment_text = script[match.start():match.end()] + if comment_text.startswith('/*'): + comment_text = comment_text[2:-2] + opcode = 'JSOP_C_COMMENT' + else: + comment_text = comment_text[2:] + opcode = 'JSOP_CPP_COMMENT' + opcode = opcode[5:].lower() + + start_offset = match.start() + end_offset = match.end()-1 + + start_pos = node_positions.from_offset(start_offset) + end_pos = node_positions.from_offset(end_offset) + kwargs = { + 'type': 'COMMENT', + 'atom': comment_text, + 'opcode': opcode, + '_start_line': start_pos.line, + '_start_col': start_pos.col, + '_end_line': end_pos.line, + '_end_col': end_pos.col, + 'parent': None, + 'kids': [], + 'node_index': None + } + comment_node = _Node() + comment_node.__dict__.update(kwargs) + comments.append(comment_node) + + # Start searching immediately after the start of the comment in case + # this one was within a string or a regexp. + pos = match.start()+1 + +def parse(script, error_callback, startpos=None): + """ All node positions will be relative to startpos. This allows scripts + to be embedded in a file (for example, HTML). + """ + def _wrapped_callback(line, col, msg): + assert msg.startswith('JSMSG_') + msg = msg[6:].lower() + error_callback(line, col, msg) + + startpos = startpos or NodePos(0,0) + return spidermonkey.parse(script, _Node, _wrapped_callback, + startpos.line, startpos.col) + +def filtercomments(possible_comments, node_positions, root_node): + comment_ignore_ranges = NodeRanges() + + def process(node): + if node.kind == tok.NUMBER: + node.atom = node_positions.text(node.start_pos(), node.end_pos()) + elif node.kind == tok.STRING or \ + (node.kind == tok.OBJECT and node.opcode == op.REGEXP): + start_offset = node_positions.to_offset(node.start_pos()) + end_offset = node_positions.to_offset(node.end_pos()) - 1 + comment_ignore_ranges.add(start_offset, end_offset) + for kid in node.kids: + if kid: + process(kid) + process(root_node) + + comments = [] + for comment in possible_comments: + start_offset = node_positions.to_offset(comment.start_pos()) + end_offset = node_positions.to_offset(comment.end_pos()) + if comment_ignore_ranges.has(start_offset): + continue + comment_ignore_ranges.add(start_offset, end_offset) + comments.append(comment) + return comments + +def findcomments(script, root_node, start_pos=None): + node_positions = NodePositions(script, start_pos) + possible_comments = findpossiblecomments(script, node_positions) + return filtercomments(possible_comments, node_positions, root_node) + +def is_compilable_unit(script): + return spidermonkey.is_compilable_unit(script) + +def _dump_node(node, depth=0): + if node is None: + print ' '*depth, + print '(None)' + print + else: + print ' '*depth, + print '%s, %s' % (_tok_names[node.kind], _op_names[node.opcode]) + print ' '*depth, + print '%s - %s' % (node.start_pos(), node.end_pos()) + if hasattr(node, 'atom'): + print ' '*depth, + print 'atom: %s' % node.atom + if node.no_semi: + print ' '*depth, + print '(no semicolon)' + print + for node in node.kids: + _dump_node(node, depth+1) + +def dump_tree(script): + def error_callback(line, col, msg): + print '(%i, %i): %s', (line, col, msg) + node = parse(script, error_callback) + _dump_node(node) + +class TestComments(unittest.TestCase): + def _test(self, script, expected_comments): + root = parse(script, lambda line, col, msg: None) + comments = findcomments(script, root) + encountered_comments = [node.atom for node in comments] + self.assertEquals(encountered_comments, list(expected_comments)) + def testSimpleComments(self): + self._test('re = /\//g', ()) + self._test('re = /\///g', ()) + self._test('re = /\////g', ('g',)) + def testCComments(self): + self._test('/*a*//*b*/', ('a', 'b')) + self._test('/*a\r\na*//*b\r\nb*/', ('a\r\na', 'b\r\nb')) + self._test('a//*b*/c', ('*b*/c',)) + self._test('a///*b*/c', ('/*b*/c',)) + self._test('a/*//*/;', ('//',)) + self._test('a/*b*/+/*c*/d', ('b', 'c')) + +class TestNodePositions(unittest.TestCase): + def _test(self, text, expected_lines, expected_cols): + # Get a NodePos list + positions = NodePositions(text) + positions = [positions.from_offset(i) for i in range(0, len(text))] + encountered_lines = ''.join([str(x.line) for x in positions]) + encountered_cols = ''.join([str(x.col) for x in positions]) + self.assertEquals(encountered_lines, expected_lines.replace(' ', '')) + self.assertEquals(encountered_cols, expected_cols.replace(' ', '')) + def testSimple(self): + self._test( + 'abc\r\ndef\nghi\n\nj', + '0000 0 1111 2222 3 4', + '0123 4 0123 0123 0 0' + ) + self._test( + '\rabc', + '0 111', + '0 012' + ) + def testText(self): + pos = NodePositions('abc\r\ndef\n\nghi') + self.assertEquals(pos.text(NodePos(0, 0), NodePos(0, 0)), 'a') + self.assertEquals(pos.text(NodePos(0, 0), NodePos(0, 2)), 'abc') + self.assertEquals(pos.text(NodePos(0, 2), NodePos(1, 2)), 'c\r\ndef') + def testOffset(self): + pos = NodePositions('abc\r\ndef\n\nghi') + self.assertEquals(pos.to_offset(NodePos(0, 2)), 2) + self.assertEquals(pos.to_offset(NodePos(1, 0)), 5) + self.assertEquals(pos.to_offset(NodePos(3, 1)), 11) + def testStartPos(self): + pos = NodePositions('abc\r\ndef\n\nghi', NodePos(3,4)) + self.assertEquals(pos.to_offset(NodePos(3, 4)), 0) + self.assertEquals(pos.to_offset(NodePos(3, 5)), 1) + self.assertEquals(pos.from_offset(0), NodePos(3, 4)) + self.assertEquals(pos.text(NodePos(3, 4), NodePos(3, 4)), 'a') + self.assertEquals(pos.text(NodePos(3, 4), NodePos(3, 6)), 'abc') + self.assertEquals(pos.text(NodePos(3, 6), NodePos(4, 2)), 'c\r\ndef') + +class TestNodeRanges(unittest.TestCase): + def testAdd(self): + r = NodeRanges() + r.add(5, 10) + self.assertEquals(r._offsets, [5,11]) + r.add(15, 20) + self.assertEquals(r._offsets, [5,11,15,21]) + r.add(21,22) + self.assertEquals(r._offsets, [5,11,15,23]) + r.add(4,5) + self.assertEquals(r._offsets, [4,11,15,23]) + r.add(9,11) + self.assertEquals(r._offsets, [4,12,15,23]) + r.add(10,20) + self.assertEquals(r._offsets, [4,23]) + r.add(4,22) + self.assertEquals(r._offsets, [4,23]) + r.add(30,30) + self.assertEquals(r._offsets, [4,23,30,31]) + def testHas(self): + r = NodeRanges() + r.add(5, 10) + r.add(15, 15) + assert not r.has(4) + assert r.has(5) + assert r.has(6) + assert r.has(9) + assert r.has(10) + assert not r.has(14) + assert r.has(15) + assert not r.has(16) + +class TestCompilableUnit(unittest.TestCase): + def test(self): + tests = ( + ('var s = "', False), + ('bogon()', True), + ('int syntax_error;', True), + ('a /* b', False), + ('re = /.*', False), + ('{ // missing curly', False) + ) + for text, result in tests: + self.assertEquals(is_compilable_unit(text), result) + # NOTE: This seems like a bug. + self.assert_(is_compilable_unit("/* test")) + +class TestLineOffset(unittest.TestCase): + def testErrorPos(self): + def geterror(script, startpos): + errors = [] + def onerror(line, col, msg): + errors.append((line, col, msg)) + parse(script, onerror, startpos) + self.assertEquals(len(errors), 1) + return errors[0] + self.assertEquals(geterror(' ?', None), (0, 1, 'syntax_error')) + self.assertEquals(geterror('\n ?', None), (1, 1, 'syntax_error')) + self.assertEquals(geterror(' ?', NodePos(1,1)), (1, 2, 'syntax_error')) + self.assertEquals(geterror('\n ?', NodePos(1,1)), (2, 1, 'syntax_error')) + def testNodePos(self): + def getnodepos(script, startpos): + root = parse(script, None, startpos) + self.assertEquals(root.kind, tok.LC) + var, = root.kids + self.assertEquals(var.kind, tok.VAR) + return var.start_pos() + self.assertEquals(getnodepos('var x;', None), NodePos(0,0)) + self.assertEquals(getnodepos(' var x;', None), NodePos(0,1)) + self.assertEquals(getnodepos('\n\n var x;', None), NodePos(2,1)) + self.assertEquals(getnodepos('var x;', NodePos(3,4)), NodePos(3,4)) + self.assertEquals(getnodepos(' var x;', NodePos(3,4)), NodePos(3,5)) + self.assertEquals(getnodepos('\n\n var x;', NodePos(3,4)), NodePos(5,1)) + def testComments(self): + def testcomment(comment, startpos, expectedpos): + root = parse(comment, None, startpos) + comment, = findcomments(comment, root, startpos) + self.assertEquals(comment.start_pos(), expectedpos) + for comment in ('/*comment*/', '//comment'): + testcomment(comment, None, NodePos(0,0)) + testcomment(' %s' % comment, None, NodePos(0,1)) + testcomment('\n\n %s' % comment, None, NodePos(2,1)) + testcomment('%s' % comment, NodePos(3,4), NodePos(3,4)) + testcomment(' %s' % comment, NodePos(3,4), NodePos(3,5)) + testcomment('\n\n %s' % comment, NodePos(3,4), NodePos(5,1)) + +if __name__ == '__main__': + unittest.main() + Property changes on: trunk/javascriptlint/jsparse.py ___________________________________________________________________ Added: svn:mergeinfo + Added: svn:eol-style + native Copied: trunk/javascriptlint/lint.py (from rev 254, trunk/javascriptlint/pyjsl/lint.py) =================================================================== --- trunk/javascriptlint/lint.py (rev 0) +++ trunk/javascriptlint/lint.py 2009-10-03 16:53:02 UTC (rev 255) @@ -0,0 +1,505 @@ +#!/usr/bin/python +# vim: ts=4 sw=4 expandtab +import os.path +import re + +import conf +import htmlparse +import jsparse +import visitation +import warnings +import util + +from spidermonkey import tok, op + +_newline_kinds = ( + 'eof', 'comma', 'dot', 'semi', 'colon', 'lc', 'rc', 'lp', 'rb', 'assign', + 'relop', 'hook', 'plus', 'minus', 'star', 'divop', 'eqop', 'shop', 'or', + 'and', 'bitor', 'bitxor', 'bitand', 'else', 'try' +) + +_globals = frozenset([ + 'Array', 'Boolean', 'Math', 'Number', 'String', 'RegExp', 'Script', 'Date', + 'isNaN', 'isFinite', 'parseFloat', 'parseInt', + 'eval', 'NaN', 'Infinity', + 'escape', 'unescape', 'uneval', + 'decodeURI', 'encodeURI', 'decodeURIComponent', 'encodeURIComponent', + 'Function', 'Object', + 'Error', 'InternalError', 'EvalError', 'RangeError', 'ReferenceError', + 'SyntaxError', 'TypeError', 'URIError', + 'arguments', 'undefined' +]) + +def _find_function(node): + while node and node.kind != tok.FUNCTION: + node = node.parent + return node + +def _find_functions(node): + functions = [] + while node: + if node.kind == tok.FUNCTION: + functions.append(node) + node = node.parent + return functions + +def _parse_control_comment(comment): + """ Returns None or (keyword, parms) """ + if comment.atom.lower().startswith('jsl:'): + control_comment = comment.atom[4:] + elif comment.atom.startswith('@') and comment.atom.endswith('@'): + control_comment = comment.atom[1:-1] + else: + return None + + control_comments = { + 'ignoreall': (False), + 'ignore': (False), + 'end': (False), + 'option explicit': (False), + 'import': (True), + 'fallthru': (False), + 'pass': (False), + 'declare': (True) + } + if control_comment.lower() in control_comments: + keyword = control_comment.lower() + else: + keyword = control_comment.lower().split()[0] + if not keyword in control_comments: + return None + + parms = control_comment[len(keyword):].strip() + return (comment, keyword, parms) + +class Scope: + """ Outer-level scopes will never be associated with a node. + Inner-level scopes will always be associated with a node. + """ + def __init__(self): + self._parent = None + self._kids = [] + self._identifiers = {} + self._references = [] + self._node = None + def add_scope(self, node): + assert not node is None + self._kids.append(Scope()) + self._kids[-1]._parent = self + self._kids[-1]._node = node + return self._kids[-1] + def add_declaration(self, name, node): + self._identifiers[name] = node + def add_reference(self, name, node): + self._references.append((name, node)) + def get_identifier(self, name): + if name in self._identifiers: + return self._identifiers[name] + else: + return None + def get_identifiers(self): + "returns a list of names" + return self._identifiers.keys() + def resolve_identifier(self, name): + if name in self._identifiers: + return self, self._identifiers[name] + if self._parent: + return self._parent.resolve_identifier(name) + return None + def get_unreferenced_and_undeclared_identifiers(self): + """ Returns a tuple of unreferenced and undeclared, where each is a list + of (scope, name, node) tuples. + """ + unreferenced = {} + undeclared = [] + self._find_unreferenced_and_undeclared(unreferenced, undeclared, False) + + # Convert "unreferenced" from a dictionary of: + # { (scope, name): node } + # to a list of: + # [ (scope, name, node) ] + # sorted by node position. + unreferenced = [(key[0], key[1], node) for key, node + in unreferenced.items()] + unreferenced.sort(key=lambda x: x[2].start_pos()) + + return unreferenced, undeclared + def _find_unreferenced_and_undeclared(self, unreferenced, undeclared, + is_in_with_scope): + """ unreferenced is a dictionary, such that: + (scope, name): node + } + undeclared is a list, such that: [ + (scope, name, node) + ] + """ + if self._node and self._node.kind == tok.WITH: + is_in_with_scope = True + + # Add all identifiers as unreferenced. Children scopes will remove + # them if they are referenced. Variables need to be keyed by name + # instead of node, because function parameters share the same node. + for name, node in self._identifiers.items(): + unreferenced[(self, name)] = node + + # Remove all declared variables from the "unreferenced" set; add all + # undeclared variables to the "undeclared" list. + for name, node in self._references: + resolved = self.resolve_identifier(name) + if resolved: + # Make sure this isn't an assignment. + if node.parent.kind in (tok.ASSIGN, tok.INC, tok.DEC) and \ + node.node_index == 0 and \ + node.parent.parent.kind == tok.SEMI: + continue + unreferenced.pop((resolved[0], name), None) + else: + # with statements cannot have undeclared identifiers. + if not is_in_with_scope: + undeclared.append((self, name, node)) + + for child in self._kids: + child._find_unreferenced_and_undeclared(unreferenced, undeclared, + is_in_with_scope) + def find_scope(self, node): + for kid in self._kids: + scope = kid.find_scope(node) + if scope: + return scope + + # Always add it to the outer scope. + if not self._parent: + assert not self._node + return self + + # Conditionally add it to an inner scope. + assert self._node + if (node.start_pos() >= self._node.start_pos() and \ + node.end_pos() <= self._node.end_pos()): + return self + +class _Script: + def __init__(self): + self._imports = set() + self.scope = Scope() + def importscript(self, script): + self._imports.add(script) + def hasglobal(self, name): + return not self._findglobal(name, set()) is None + def _findglobal(self, name, searched): + """ searched is a set of all searched scripts """ + # Avoid recursion. + if self in searched: + return + + # Check this scope. + if self.scope.get_identifier(name): + return self + searched.add(self) + + # Search imported scopes. + for script in self._imports: + global_ = script._findglobal(name, searched) + if global_: + return global_ + +def lint_files(paths, lint_error, conf=conf.Conf()): + def lint_file(path, kind): + def import_script(import_path): + # The user can specify paths using backslashes (such as when + # linting Windows scripts on a posix environment. + import_path = import_path.replace('\\', os.sep) + import_path = os.path.join(os.path.dirname(path), import_path) + return lint_file(import_path, 'js') + def _lint_error(*args): + return lint_error(normpath, *args) + + normpath = util.normpath(path) + if normpath in lint_cache: + return lint_cache[normpath] + print normpath + contents = util.readfile(path) + lint_cache[normpath] = _Script() + + script_parts = [] + if kind == 'js': + script_parts.append((None, contents)) + elif kind == 'html': + for script in htmlparse.findscripts(contents): + if script['src']: + other = import_script(script['src']) + lint_cache[normpath].importscript(other) + if script['script'].strip(): + script_parts.append((script['startpos'], script['script'])) + else: + assert False, 'Unsupported file kind: %s' % kind + + _lint_script_parts(script_parts, lint_cache[normpath], _lint_error, conf, import_script) + return lint_cache[normpath] + + lint_cache = {} + for path in paths: + ext = os.path.splitext(path)[1] + if ext.lower() in ['.htm', '.html']: + lint_file(path, 'html') + else: + lint_file(path, 'js') + +def _lint_script_part(scriptpos, script, script_cache, conf, ignores, + report_native, report_lint, import_callback): + def parse_error(row, col, msg): + if not msg in ('anon_no_return_value', 'no_return_value', + 'redeclared_var', 'var_hides_arg'): + parse_errors.append((jsparse.NodePos(row, col), msg)) + + def report(node, errname, pos=None, **errargs): + if errname == 'empty_statement' and node.kind == tok.LC: + for pass_ in passes: + if pass_.start_pos() > node.start_pos() and \ + pass_.end_pos() < node.end_pos(): + passes.remove(pass_) + return + + if errname == 'missing_break': + # Find the end of the previous case/default and the beginning of + # the next case/default. + assert node.kind in (tok.CASE, tok.DEFAULT) + prevnode = node.parent.kids[node.node_index-1] + expectedfallthru = prevnode.end_pos(), node.start_pos() + elif errname == 'missing_break_for_last_case': + # Find the end of the current case/default and the end of the + # switch. + assert node.parent.kind == tok.LC + expectedfallthru = node.end_pos(), node.parent.end_pos() + else: + expectedfallthru = None + + if expectedfallthru: + start, end = expectedfallthru + for fallthru in fallthrus: + # Look for a fallthru between the end of the current case or + # default statement and the beginning of the next token. + if fallthru.start_pos() > start and fallthru.end_pos() < end: + fallthrus.remove(fallthru) + return + + report_lint(node, errname, pos, **errargs) + + parse_errors = [] + declares = [] + import_paths = [] + fallthrus = [] + passes = [] + + node_positions = jsparse.NodePositions(script, scriptpos) + possible_comments = jsparse.findpossiblecomments(script, node_positions) + + root = jsparse.parse(script, parse_error, scriptpos) + if not root: + # Report errors and quit. + for pos, msg in parse_errors: + report_native(pos, msg) + return + + comments = jsparse.filtercomments(possible_comments, node_positions, root) + start_ignore = None + for comment in comments: + cc = _parse_control_comment(comment) + if cc: + node, keyword, parms = cc + if keyword == 'declare': + if not util.isidentifier(parms): + report(node, 'jsl_cc_not_understood') + else: + declares.append((parms, node)) + elif keyword == 'ignore': + if start_ignore: + report(node, 'mismatch_ctrl_comments') + else: + start_ignore = node + elif keyword == 'end': + if start_ignore: + ignores.append((start_ignore.start_pos(), node.end_pos())) + start_ignore = None + else: + report(node, 'mismatch_ctrl_comments') + elif keyword == 'import': + if not parms: + report(node, 'jsl_cc_not_understood') + else: + import_paths.append(parms) + elif keyword == 'fallthru': + fallthrus.append(node) + elif keyword == 'pass': + passes.append(node) + else: + if comment.opcode == 'c_comment': + # Look for nested C-style comments. + nested_comment = comment.atom.find('/*') + if nested_comment < 0 and comment.atom.endswith('/'): + nested_comment = len(comment.atom) - 1 + # Report at the actual error of the location. Add two + # characters for the opening two characters. + if nested_comment >= 0: + pos = node_positions.from_offset(node_positions.to_offset(comment.start_pos()) + 2 + nested_comment) + report(comment, 'nested_comment', pos=pos) + if comment.atom.lower().startswith('jsl:'): + report(comment, 'jsl_cc_not_understood') + elif comment.atom.startswith('@'): + report(comment, 'legacy_cc_not_understood') + if start_ignore: + report(start_ignore, 'mismatch_ctrl_comments') + + # Wait to report parse errors until loading jsl:ignore directives. + for pos, msg in parse_errors: + report_native(pos, msg) + + # Find all visitors and convert them into "onpush" callbacks that call "report" + visitors = { + 'push': warnings.make_visitors() + } + for event in visitors: + for kind, callbacks in visitors[event].items(): + visitors[event][kind] = [_getreporter(callback, report) for callback in callbacks] + + # Push the scope/variable checks. + visitation.make_visitors(visitors, [_get_scope_checks(script_cache.scope, report)]) + + # kickoff! + _lint_node(root, visitors) + + for fallthru in fallthrus: + report(fallthru, 'invalid_fallthru') + for fallthru in passes: + report(fallthru, 'invalid_pass') + + # Process imports by copying global declarations into the universal scope. + for path in import_paths: + script_cache.importscript(import_callback(path)) + + for name, node in declares: + declare_scope = script_cache.scope.find_scope(node) + if declare_scope.get_identifier(name): + report(node, 'redeclared_var', name=name) + else: + declare_scope.add_declaration(name, node) + +def _lint_script_parts(script_parts, script_cache, lint_error, conf, import_callback): + def report_lint(node, errname, pos=None, **errargs): + errdesc = warnings.format_error(errname, **errargs) + _report(pos or node.start_pos(), errname, errdesc, True) + + def report_native(pos, errname): + # TODO: Format the error. + _report(pos, errname, errname, False) + + def _report(pos, errname, errdesc, require_key): + try: + if not conf[errname]: + return + except KeyError, err: + if require_key: + raise + + for start, end in ignores: + if pos >= start and pos <= end: + return + + return lint_error(pos.line, pos.col, errname, errdesc) + + for scriptpos, script in script_parts: + ignores = [] + _lint_script_part(scriptpos, script, script_cache, conf, ignores, + report_native, report_lint, import_callback) + + scope = script_cache.scope + unreferenced, undeclared = scope.get_unreferenced_and_undeclared_identifiers() + for decl_scope, name, node in undeclared: + if name in conf['declarations']: + continue + if name in _globals: + continue + if not script_cache.hasglobal(name): + report_lint(node, 'undeclared_identifier', name=name) + for ref_scope, name, node in unreferenced: + # Ignore the outer scope. + if ref_scope != scope: + report_lint(node, 'unreferenced_identifier', name=name) + +def _getreporter(visitor, report): + def onpush(node): + try: + ret = visitor(node) + assert ret is None, 'visitor should raise an exception, not return a value' + except warnings.LintWarning, warning: + # TODO: This is ugly hardcoding to improve the error positioning of + # "missing_semicolon" errors. + if visitor.warning in ('missing_semicolon', 'missing_semicolon_for_lambda'): + pos = warning.node.end_pos() + else: + pos = None + report(warning.node, visitor.warning, pos=pos, **warning.errargs) + return onpush + +def _warn_or_declare(scope, name, node, report): + parent_scope, other = scope.resolve_identifier(name) or (None, None) + if other and other.kind == tok.FUNCTION and name in other.fn_args: + report(node, 'var_hides_arg', name=name) + elif other and parent_scope == scope: + report(node, 'redeclared_var', name=name) + else: + # TODO: Warn when hiding a variable in a parent scope. + scope.add_declaration(name, node) + +def _get_scope_checks(scope, report): + scopes = [scope] + + class scope_checks: + ' ' + @visitation.visit('push', tok.NAME) + def _name(self, node): + if node.node_index == 0 and node.parent.kind == tok.COLON and node.parent.parent.kind == tok.RC: + return # left side of object literal + if node.parent.kind == tok.VAR: + _warn_or_declare(scopes[-1], node.atom, node, report) + return + if node.parent.kind == tok.CATCH: + scopes[-1].add_declaration(node.atom, node) + scopes[-1].add_reference(node.atom, node) + + @visitation.visit('push', tok.FUNCTION) + def _push_func(self, node): + if node.fn_name: + _warn_or_declare(scopes[-1], node.fn_name, node, report) + self._push_scope(node) + for var_name in node.fn_args: + scopes[-1].add_declaration(var_name, node) + + @visitation.visit('push', tok.LEXICALSCOPE, tok.WITH) + def _push_scope(self, node): + scopes.append(scopes[-1].add_scope(node)) + + @visitation.visit('pop', tok.FUNCTION, tok.LEXICALSCOPE, tok.WITH) + def _pop_scope(self, node): + scopes.pop() + + return scope_checks + + +def _lint_node(node, visitors): + + for kind in (node.kind, (node.kind, node.opcode)): + if kind in visitors['push']: + for visitor in visitors['push'][kind]: + visitor(node) + + for child in node.kids: + if child: + _lint_node(child, visitors) + + for kind in (node.kind, (node.kind, node.opcode)): + if kind in visitors['pop']: + for visitor in visitors['pop'][kind]: + visitor(node) + + Property changes on: trunk/javascriptlint/lint.py ___________________________________________________________________ Added: svn:mergeinfo + Added: svn:eol-style + native Copied: trunk/javascriptlint/spidermonkey.py (from rev 254, trunk/javascriptlint/pyjsl/spidermonkey.py) =================================================================== --- trunk/javascriptlint/spidermonkey.py (rev 0) +++ trunk/javascriptlint/spidermonkey.py 2009-10-03 16:53:02 UTC (rev 255) @@ -0,0 +1,10 @@ +# vim: ts=4 sw=4 expandtab + +# This is a wrapper script to make it easier for development. It tries to +# import the development version first, and if that fails, it goes after the +# real version. +try: + from spidermonkey_ import * +except ImportError: + from pyspidermonkey import * + Property changes on: trunk/javascriptlint/spidermonkey.py ___________________________________________________________________ Added: svn:mergeinfo + Added: svn:eol-style + native Copied: trunk/javascriptlint/spidermonkey_.py (from rev 254, trunk/javascriptlint/pyjsl/spidermonkey_.py) =================================================================== --- trunk/javascriptlint/spidermonkey_.py (rev 0) +++ trunk/javascriptlint/spidermonkey_.py 2009-10-03 16:53:02 UTC (rev 255) @@ -0,0 +1,20 @@ +# vim: ts=4 sw=4 expandtab +from distutils.core import setup, Extension +import os +import sys + +# Add the bin directory to the module search path +def _get_lib_path(): + import distutils.dist + import distutils.command.build + dist = distutils.dist.Distribution() + build = distutils.command.build.build(dist) + build.finalize_options() + return os.path.join(os.path.dirname(__file__), '..', build.build_platlib, 'javascriptlint') + +sys.path.insert(0, _get_lib_path()) +try: + from pyspidermonkey import * +finally: + sys.path.pop(0) + Property changes on: trunk/javascriptlint/spidermonkey_.py ___________________________________________________________________ Added: svn:mergeinfo + Added: svn:eol-style + native Copied: trunk/javascriptlint/util.py (from rev 254, trunk/javascriptlint/pyjsl/util.py) =================================================================== --- trunk/javascriptlint/util.py (rev 0) +++ trunk/javascriptlint/util.py 2009-10-03 16:53:02 UTC (rev 255) @@ -0,0 +1,97 @@ +# vim: ts=4 sw=4 expandtab +import codecs +import os.path +import re +import unittest + +_identifier = re.compile('^[A-Za-z_$][A-Za-z0-9_$]*$') + +def isidentifier(text): + return _identifier.match(text) + +def _encode_error_keyword(s): + s = s.replace('\\', '\\\\') + s = s.replace('"', '\\"') + s = s.replace("'", "\\'") + s = s.replace("\t", "\\t") + s = s.replace("\r", "\\r") + s = s.replace("\n", "\\n") + return s + +def format_error(output_format, path, line, col, errname, errdesc): + errprefix = 'warning' #TODO + replacements = { + '__FILE__': path, + '__FILENAME__': os.path.basename(path), + '__LINE__': str(line+1), + '__COL__': str(col), + '__ERROR__': '%s: %s' % (errprefix, errdesc), + '__ERROR_NAME__': errname, + '__ERROR_PREFIX__': errprefix, + '__ERROR_MSG__': errdesc, + '__ERROR_MSGENC__': errdesc, + } + + formatted_error = output_format + + # If the output format starts with encode:, all of the keywords should be + # encoded. + if formatted_error.startswith('encode:'): + formatted_error = formatted_error[len('encode:'):] + encoded_keywords = replacements.keys() + else: + encoded_keywords = ['__ERROR_MSGENC__'] + + for keyword in encoded_keywords: + replacements[keyword] = _encode_error_keyword(replacements[keyword]) + + regexp = '|'.join(replacements.keys()) + return re.sub(regexp, lambda match: replacements[match.group(0)], + formatted_error) + +def readfile(path): + file = codecs.open(path, 'r', 'utf-8') + contents = file.read() + if contents and contents[0] == unicode(codecs.BOM_UTF8, 'utf8'): + contents = contents[1:] + return contents + +def normpath(path): + path = os.path.abspath(path) + path = os.path.normcase(path) + path = os.path.normpath(path) + return path + +class TestUtil(unittest.TestCase): + def testIdentifier(self): + assert not isidentifier('') + assert not isidentifier('0a') + assert not isidentifier('a b') + assert isidentifier('a') + assert isidentifier('$0') + + def testEncodeKeyword(self): + self.assertEquals(_encode_error_keyword(r'normal text'), 'normal text') + self.assertEquals(_encode_error_keyword(r'a\b'), r'a\\b') + self.assertEquals(_encode_error_keyword(r"identifier's"), r"identifier\'s") + self.assertEquals(_encode_error_keyword(r'"i"'), r'\"i\"') + self.assertEquals(_encode_error_keyword('a\tb'), r'a\tb') + self.assertEquals(_encode_error_keyword('a\rb'), r'a\rb') + self.assertEquals(_encode_error_keyword('a\nb'), r'a\nb') + + def testFormattedError(self): + self.assertEquals(format_error('__FILE__', '__LINE__', 1, 2, 'name', 'desc'), + '__LINE__') + self.assertEquals(format_error('__FILE__', r'c:\my\file', 1, 2, 'name', 'desc'), + r'c:\my\file') + self.assertEquals(format_error('encode:__FILE__', r'c:\my\file', 1, 2, 'name', 'desc'), + r'c:\\my\\file') + self.assertEquals(format_error('__ERROR_MSGENC__', r'c:\my\file', 1, 2, 'name', r'a\b'), + r'a\\b') + self.assertEquals(format_error('encode:__ERROR_MSGENC__', r'c:\my\file', 1, 2, 'name', r'a\b'), + r'a\\b') + + +if __name__ == '__main__': + unittest.main() + Property changes on: trunk/javascriptlint/util.py ___________________________________________________________________ Added: svn:mergeinfo + Added: svn:eol-style + native Copied: trunk/javascriptlint/visitation.py (from rev 254, trunk/javascriptlint/pyjsl/visitation.py) =================================================================== --- trunk/javascriptlint/visitation.py (rev 0) +++ trunk/javascriptlint/visitation.py 2009-10-03 16:53:02 UTC (rev 255) @@ -0,0 +1,52 @@ +# vim: ts=4 sw=4 expandtab +""" This is an abstract module for visiting specific nodes. This is useed to +traverse the tree to generate warnings. +""" + +def visit(event, *args): + """ This decorator is used to indicate which nodes the function should + examine. The function should accept (self, node) and return the relevant + node or None. """ + def _decorate(fn): + fn._visit_event = event + fn._visit_nodes = args + return fn + return _decorate + +def make_visitors(visitors, klasses): + """ Searches klasses for all member functions decorated with @visit and + fills a dictionary that looks like: + visitors = { + 'event_name': { + 'node_type' : [func1, func2] + } + } + """ + assert isinstance(visitors, dict) + + # Intantiate an instance of each class + for klass in klasses: + if klass.__name__.lower() != klass.__name__: + raise ValueError, 'class names must be lowercase' + if not klass.__doc__: + raise ValueError, 'missing docstring on class' + + # Look for functions with the "_visit_nodes" property. + visitor = klass() + for func in [getattr(visitor, name) for name in dir(visitor)]: + event_visitors = None + for node_kind in getattr(func, '_visit_nodes', ()): + # Group visitors by event (e.g. push vs pop) + if not event_visitors: + try: + event_visitors = visitors[func._visit_event] + except KeyError: + event_visitors = visitors[func._visit_event] = {} + + # Map from node_kind to the function + try: + event_visitors[node_kind].append(func) + except KeyError: + event_visitors[node_kind] = [func] + return visitors + Property changes on: trunk/javascriptlint/visitation.py ___________________________________________________________________ Added: svn:mergeinfo + Added: svn:eol-style + native Copied: trunk/javascriptlint/warnings.py (from rev 254, trunk/javascriptlint/pyjsl/warnings.py) =================================================================== --- trunk/javascriptlint/warnings.py (rev 0) +++ trunk/javascriptlint/warnings.py 2009-10-03 16:53:02 UTC (rev 255) @@ -0,0 +1,611 @@ +# vim: ts=4 sw=4 expandtab +""" This module contains all the warnings. To add a new warning, define a +function. Its name should be in lowercase and words should be separated by +underscores. + +The function should be decorated with a @lookfor call specifying the nodes it +wants to examine. The node names may be in the tok.KIND or (tok.KIND, op.OPCODE) +format. To report a warning, the function should raise a LintWarning exception. + +For example: + + @lookfor(tok.NODEKIND, (tok.NODEKIND, op.OPCODE)) + def warning_name(node): + if questionable: + raise LintWarning, node +""" +import re +import sys +import types + +import util +import visitation + +from spidermonkey import tok, op + +_ALL_TOKENS = tuple(filter(lambda x: x != tok.EOF, tok.__dict__.values())) + +def _get_assigned_lambda(node): + """ Given a node "x = function() {}", returns "function() {}". + """ + value = None + if node.kind == tok.SEMI: + assign_node, = node.kids + if assign_node and assign_node.kind == tok.ASSIGN: + ignored, value = assign_node.kids + elif node.kind == tok.VAR: + variables = node.kids + if variables: + value, = variables[-1].kids + + if value and value.kind == tok.FUNCTION and value.opcode == op.ANONFUNOBJ: + return value + +# TODO: document inspect, node:opcode, etc + +warnings = { + 'comparison_type_conv': 'comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==)', + 'default_not_at_end': 'the default case is not at the end of the switch statement', + 'duplicate_case_in_switch': 'duplicate case in switch statement', + 'missing_default_case': 'missing default case in switch statement', + 'with_statement': 'with statement hides undeclared variables; use temporary variable instead', + 'useless_comparison': 'useless comparison; comparing identical expressions', + 'use_of_label': 'use of label', + 'misplaced_regex': 'regular expressions should be preceded by a left parenthesis, assignment, colon, or comma', + 'assign_to_function_call': 'assignment to a function call', + 'ambiguous_else_stmt': 'the else statement could be matched with one of multiple if statements (use curly braces to indicate intent', + 'block_without_braces': 'block statement without curly braces', + 'ambiguous_nested_stmt': 'block statements containing block statements should use curly braces to resolve ambiguity', + 'inc_dec_within_stmt': 'increment (++) and decrement (--) operators used as part of greater statement', + 'comma_separated_stmts': 'multiple statements separated by commas (use semicolons?)', + 'empty_statement': 'empty statement or extra semicolon', + 'missing_break': 'missing break statement', + 'missing_break_for_last_case': 'missing break statement for last case in switch', + 'multiple_plus_minus': 'unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs', + 'useless_assign': 'useless assignment', + 'unreachable_code': 'unreachable code', + 'meaningless_block': 'meaningless block; curly braces have no impact', + 'useless_void': 'use of the void type may be unnecessary (void is always undefined)', + 'parseint_missing_radix': 'parseInt missing radix parameter', + 'leading_decimal_point': 'leading decimal point may indicate a number or an object member', + 'trailing_decimal_point': 'trailing decimal point may indicate a number or an object member', + 'octal_number': 'leading zeros make an octal number', + 'trailing_comma_in_array': 'extra comma is not recommended in array initializers', + 'useless_quotes': 'the quotation marks are unnecessary', + 'mismatch_ctrl_comments': 'mismatched control comment; "ignore" and "end" control comments must have a one-to-one correspondence', + 'redeclared_var': 'redeclaration of {name}', + 'undeclared_identifier': 'undeclared identifier: {name}', + 'unreferenced_identifier': 'identifier is declared but never referenced: {name}', + 'jsl_cc_not_understood': 'couldn\'t understand control comment using /*jsl:keyword*/ syntax', + 'nested_comment': 'nested comment', + 'legacy_cc_not_understood': 'couldn\'t understand control comment using /*@keyword@*/ syntax', + 'var_hides_arg': 'variable {name} hides argument', + 'duplicate_formal': 'TODO', + 'missing_semicolon': 'missing semicolon', + 'missing_semicolon_for_lambda': 'missing semicolon for lambda assignment', + 'ambiguous_newline': 'unexpected end of line; it is ambiguous whether these lines are part of the same statement', + 'missing_option_explicit': 'the "option explicit" control comment is missing', + 'partial_option_explicit': 'the "option explicit" control comment, if used, must be in the first script tag', + 'dup_option_explicit': 'duplicate "option explicit" control comment', + 'invalid_fallthru': 'unexpected "fallthru" control comment', + 'invalid_pass': 'unexpected "pass" control comment', + 'want_assign_or_call': 'expected an assignment or function call', + 'no_return_value': 'function {name} does not always return a value', + 'anon_no_return_value': 'anonymous function does not always return value' +} + +def format_error(errname, **errargs): + errdesc = warnings[errname] + try: + errdesc = re.sub(r"{(\w+)}", lambda match: errargs[match.group(1)], errdesc) + except (TypeError, KeyError):... [truncated message content] |