#!/usr/bin/env python # # capp_lint.py - Check Objective-J source code formatting, # according to Cappuccino standards: # # http://cappuccino.org/contribute/coding-style.php # # Copyright (C) 2011 Aparajita Fishman # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import with_statement from optparse import OptionParser from string import Template import cgi import cStringIO import os import os.path import re import sys import unittest EXIT_CODE_SHOW_HTML = 205 EXIT_CODE_SHOW_TOOLTIP = 206 def exit_show_html(html): sys.stdout.write(html.encode('utf-8')) sys.exit(EXIT_CODE_SHOW_HTML) def exit_show_tooltip(text): sys.stdout.write(text) sys.exit(EXIT_CODE_SHOW_TOOLTIP) def within_textmate(): return os.getenv('TM_APP_PATH') is not None def tabs2spaces(text, positions=None): while True: index = text.find(u'\t') if index < 0: return text spaces = u' ' * (4 - (index % 4)) text = text[0:index] + spaces + text[index + 1:] if positions is not None: positions.append(index) def relative_path(basedir, filename): if filename.find(basedir) == 0: filename = filename[len(basedir) + 1:] return filename def string_replacer(line): """Take string literals like 'hello' and replace them with empty string literals, while respecting escaping.""" r = [] in_quote = None escapes = 0 for i, c in enumerate(line): if in_quote: if not escapes and c == in_quote: in_quote = None r.append(c) continue # We're inside of a string literal. Ignore everything. else: if not escapes and (c == '"' or c == "'"): in_quote = c r.append(c) continue # Outside of a string literal, preserve everything. r.append(c) if c == '\\': escapes = (escapes + 1) % 2 else: escapes = 0 if in_quote: # Unterminated string literal. pass return "".join(r) class LintChecker(object): """Examine Objective-J code statically and generate warnings for possible errors and deviations from the coding-style standard. >>> LintChecker().lint_text('var b = 5+5;') [{'positions': [9], 'filename': '', 'lineNum': 1, 'message': 'binary operator without surrounding spaces', 'type': 2, 'line': u'var b = 5+5;'}] >>> LintChecker().lint_text(''' ... if( 1 ) { ... var b=7; ... c = 8; ... } ... ''') [{'positions': [2], 'filename': '', 'lineNum': 2, 'message': 'missing space between control statement and parentheses', 'type': 2, 'line': u'if( 1 ) {'}, {'positions': [8], 'filename': '', 'lineNum': 2, 'message': 'braces should be on their own line', 'type': 1, 'line': u'if( 1 ) {'}, {'positions': [3, 5], 'filename': '', 'lineNum': 2, 'message': 'space inside parentheses', 'type': 1, 'line': u'if( 1 ) {'}, {'positions': [7], 'filename': '', 'lineNum': 3, 'message': 'assignment operator without surrounding spaces', 'type': 2, 'line': u' var b=7;'}, {'lineNum': 4, 'message': 'accidental global variable', 'type': 1, 'line': u' c = 8;', 'filename': ''}] """ VAR_BLOCK_START_RE = re.compile(ur'''(?x) (?P\s*) # indent before a var keyword (?Pvar\s+) # var keyword and whitespace after (?P[a-zA-Z_$]\w*)\s* (?: (?P=)\s* (?P.*) | (?P[,;+\-/*%^&|=\\]) ) ''') SEPARATOR_RE = re.compile(ur'''(?x) (?P.*) # Everything up to the line separator (?P[,;+\-/*%^&|=\\]) # The line separator \s* # Optional whitespace after $ # End of expression ''') INDENTED_EXPRESSION_RE_TEMPLATE = ur'''(?x) [ ]{%d} # Placeholder for indent of first identifier that started block (?P.+) # Expression ''' VAR_BLOCK_RE_TEMPLATE = ur'''(?x) [ ]{%d} # Placeholder for indent of first identifier that started block (?P\s*) # Capture any further indent (?: (?P[\[\{].*) | (?P[a-zA-Z_$]\w*)\s* (?: (?P=)\s* (?P.*) | (?P[,;+\-/*%%^&|=\\]) ) | (?P.+) ) ''' STATEMENT_RE = re.compile(ur'''(?x) \s*((continue|do|for|function|if|else|return|switch|while|with)\b|\[+\s*[a-zA-Z_$]\w*\s+[a-zA-Z_$]\w*\s*[:\]]) ''') TRAILING_WHITESPACE_RE = re.compile(ur'^.*(\s+)$') STRIP_LINE_COMMENT_RE = re.compile(ur'(.*)\s*(?://.*|/\*.*\*/\s*)$') LINE_COMMENT_RE = re.compile(ur'\s*(?:/\*.*\*/\s*|//.*)$') COMMENT_RE = re.compile(ur'/\*.*?\*/') BLOCK_COMMENT_START_RE = re.compile(ur'\s*/\*.*(?!\*/\s*)$') BLOCK_COMMENT_END_RE = re.compile(ur'.*?\*/') METHOD_RE = ur'[-+]\s*\([a-zA-Z_$]\w*\)\s*[a-zA-Z_$]\w*' FUNCTION_RE = re.compile(ur'\s*function\s*(?P[a-zA-Z_$]\w*)?\(.*\)\s*\{?') RE_RE = re.compile(ur'(?]*\)\s*\w+|[a-zA-Z_$]\w*(\+\+|--)|([ -+*/%^&|<>!]=?|&&|\|\||<<|>>>|={1,3}|!==?)\s*[-+][\w(\[])'), 'pass': False}, # Also convert literals like 1.5e+7 to 42 so that the - or + in there is ignored for purposes of this warning. 'preprocess': STD_IGNORES + EXPONENTIAL_TO_SIMPLE, 'regex': re.compile(ur'(?<=[\w)\]"\']|([ ]))([-+*/%^]|&&?|\|\|?|<<|>>>?)(?=[\w({\["\']|(?(1)\b\b|[ ]))'), 'error': 'binary operator without surrounding spaces', 'showPositionForGroup': 2, 'type': ERROR_TYPE_WARNING }, { # Filter out possible = within @accessors 'filter': {'regex': re.compile(ur'^\s*(?:@outlet\s+)?[a-zA-Z_$]\w*\s+[a-zA-Z_$]\w*\s+@accessors\b'), 'pass': False}, 'preprocess': STD_IGNORES, 'regex': re.compile(ur'(?<=[\w)\]"\']|([ ]))(=|[-+*/%^&|]=|<<=|>>>?=)(?=[\w({\["\']|(?(1)\b\b|[ ]))'), 'error': 'assignment operator without surrounding spaces', 'showPositionForGroup': 2, 'type': ERROR_TYPE_WARNING }, { # Filter out @import statements and @implementation/method declarations 'filter': {'regex': re.compile(ur'^(@import\b|@implementation\b|\s*' + METHOD_RE + ')'), 'pass': False}, 'preprocess': STD_IGNORES, 'regex': re.compile(ur'(?<=[\w)\]"\']|([ ]))(===?|!==?|[<>]=?)(?=[\w({\["\']|(?(1)\b\b|[ ]))'), 'error': 'comparison operator without surrounding spaces', 'showPositionForGroup': 2, 'type': ERROR_TYPE_WARNING }, { 'regex': re.compile(ur'^(\s+)' + METHOD_RE + '|^\s*[-+](\()[a-zA-Z_$][\w]*\)\s*[a-zA-Z_$]\w*|^\s*[-+]\s*\([a-zA-Z_$][\w]*\)(\s+)[a-zA-Z_$]\w*'), 'error': 'extra or missing space in a method declaration', 'showPositionForGroup': 0, 'type': ERROR_TYPE_WARNING }, { # Check for brace following a class or method declaration 'regex': re.compile(ur'^(?:\s*[-+]\s*\([a-zA-Z_$]\w*\)|@implementation)\s*[a-zA-Z_$][\w]*.*?\s*(\{)\s*(?:$|//.*$)'), 'error': 'braces should be on their own line', 'showPositionForGroup': 0, 'type': ERROR_TYPE_ILLEGAL }, { 'regex': re.compile(ur'^\s*var\s+[a-zA-Z_$]\w*\s*=\s*function\s+([a-zA-Z_$]\w*)\s*\('), 'error': 'function name is ignored', 'showPositionForGroup': 1, 'skip': True, 'type': ERROR_TYPE_WARNING }, ) VAR_DECLARATIONS = ['none', 'single', 'strict'] VAR_DECLARATIONS_NONE = 0 VAR_DECLARATIONS_SINGLE = 1 VAR_DECLARATIONS_STRICT = 2 DIRS_TO_SKIP = ('.git', 'Frameworks', 'Build', 'Resources', 'CommonJS', 'Objective-J') ERROR_FORMATS = ('text', 'html') TEXT_ERROR_SINGLE_FILE_TEMPLATE = Template(u'$lineNum: $message.\n+$line\n') TEXT_ERROR_MULTI_FILE_TEMPLATE = Template(u'$filename:$lineNum: $message.\n+$line\n') def __init__(self, view=None, basedir='', var_declarations=VAR_DECLARATIONS_SINGLE, verbose=False): self.view = view self.basedir = unicode(basedir, 'utf-8') self.errors = [] self.errorFiles = [] self.filesToCheck = [] self.varDeclarations = var_declarations self.verbose = verbose self.sourcefile = None self.filename = u'' self.line = u'' self.lineNum = 0 self.varIndent = u'' self.identifierIndent = u'' self.fileChecklist = ( {'title': 'Check variable blocks', 'action': self.check_var_blocks}, ) def run_line_checks(self): for check in self.LINE_CHECKLIST: option = check.get('option') if option: default = check.get('optionDefault', False) if self.view and not self.view.settings().get(option, default): continue line = self.line originalLine = line lineFilter = check.get('filter') if lineFilter: match = lineFilter['regex'].search(line) if (match and not lineFilter['pass']) or (not match and lineFilter['pass']): continue preprocess = check.get('preprocess') if preprocess: if not isinstance(preprocess, (list, tuple)): preprocess = (preprocess,) for processor in preprocess: regex = processor.get('regex') if regex: line = regex.sub(processor.get('replace', ''), line) fnct = processor.get('function') if fnct: line = fnct(line) regex = check.get('regex') if not regex: continue match = regex.search(line) if not match: continue positions = [] groups = check.get('showPositionForGroup') if (check.get('id') == 'tabs'): line = tabs2spaces(line, positions=positions) elif groups is not None: line = tabs2spaces(line) if not isinstance(groups, (list, tuple)): groups = (groups,) for match in regex.finditer(line): for group in groups: if group > 0: start = match.start(group) if start >= 0: positions.append(start) else: # group 0 means show the first non-empty match for i in range(1, len(match.groups()) + 1): if match.start(i) >= 0: positions.append(match.start(i)) break if positions: self.error(check['error'], line=originalLine, positions=positions, type=check['type']) def next_statement(self, expect_line=False, check_line=True): try: while True: raw_line = self.sourcefile.next() # strip EOL if raw_line[-1] == '\n': # ... unless this is the last line which might not have a \n. raw_line = raw_line[:-1] try: self.line = unicode(raw_line, 'utf-8', 'strict') # convert to Unicode self.lineNum += 1 except UnicodeDecodeError: self.line = unicode(raw_line, 'utf-8', 'replace') self.lineNum += 1 self.error('line contains invalid unicode character(s)', type=self.ERROR_TYPE_ILLEGAL) if self.verbose: print u'%d: %s' % (self.lineNum, tabs2spaces(self.line)) if check_line: self.run_line_checks() if not self.is_statement(): continue return True except StopIteration: if expect_line: self.error('unexpected EOF', type=self.ERROR_TYPE_ILLEGAL) raise def is_statement(self): # Skip empty lines if len(self.line.strip()) == 0: return False # See if we have a line comment, skip that match = self.LINE_COMMENT_RE.match(self.line) if match: return False # Match a block comment start next so we can find its end, # otherwise we might get false matches on the contents of the block comment. match = self.BLOCK_COMMENT_START_RE.match(self.line) if match: self.block_comment() return False return True def is_expression(self): match = self.STATEMENT_RE.match(self.line) return match is None def strip_comment(self): match = self.STRIP_LINE_COMMENT_RE.match(self.expression) if match: self.expression = match.group(1) def get_expression(self, lineMatch): groupdict = lineMatch.groupdict() self.expression = groupdict.get('expression') if self.expression is None: self.expression = groupdict.get('bracket') if self.expression is None: self.expression = groupdict.get('indented_expression') if self.expression is None: self.expression = '' return # Remove all quoted strings from the expression so that we don't # count unmatched pairs inside the strings. self.expression = string_replacer(self.expression) self.strip_comment() self.expression = self.expression.strip() def block_comment(self): 'Find the end of a block comment' commentOpenCount = self.line.count('/*') commentOpenCount -= self.line.count('*/') # If there is an open comment block, eat it if commentOpenCount: if self.verbose: print u'%d: BLOCK COMMENT START' % self.lineNum else: return match = None while not match and self.next_statement(expect_line=True, check_line=False): match = self.BLOCK_COMMENT_END_RE.match(self.line) if self.verbose: print u'%d: BLOCK COMMENT END' % self.lineNum def balance_pairs(self, squareOpenCount, curlyOpenCount, parenOpenCount): # The following lines have to be indented at least as much as the first identifier # after the var keyword at the start of the block. if self.verbose: print "%d: BALANCE BRACKETS: '['=%d, '{'=%d, '('=%d" % (self.lineNum, squareOpenCount, curlyOpenCount, parenOpenCount) lineRE = re.compile(self.INDENTED_EXPRESSION_RE_TEMPLATE % len(self.identifierIndent)) while True: # If the expression has open brackets and is terminated, it's an error match = self.SEPARATOR_RE.match(self.expression) if match and match.group('separator') == ';': unterminated = [] if squareOpenCount: unterminated.append('[') if curlyOpenCount: unterminated.append('{') if parenOpenCount: unterminated.append('(') self.error('unbalanced %s' % ' and '.join(unterminated), type=self.ERROR_TYPE_ILLEGAL) return False self.next_statement(expect_line=True) match = lineRE.match(self.line) if not match: # If it doesn't match, the indent is wrong check the whole line self.error('incorrect indentation') self.expression = self.line self.strip_comment() else: # It matches, extract the expression self.get_expression(match) # Update the bracket counts squareOpenCount += self.expression.count('[') squareOpenCount -= self.expression.count(']') curlyOpenCount += self.expression.count('{') curlyOpenCount -= self.expression.count('}') parenOpenCount += self.expression.count('(') parenOpenCount -= self.expression.count(')') if squareOpenCount == 0 and curlyOpenCount == 0 and parenOpenCount == 0: if self.verbose: print u'%d: BRACKETS BALANCED' % self.lineNum # The brackets are closed, this line must be separated match = self.SEPARATOR_RE.match(self.expression) if not match: self.error('missing statement separator', type=self.ERROR_TYPE_ILLEGAL) return False return True def pairs_balanced(self, lineMatchOrBlockMatch): groups = lineMatchOrBlockMatch.groupdict() if 'assignment' in groups or 'bracket' in groups: squareOpenCount = self.expression.count('[') squareOpenCount -= self.expression.count(']') curlyOpenCount = self.expression.count('{') curlyOpenCount -= self.expression.count('}') parenOpenCount = self.expression.count('(') parenOpenCount -= self.expression.count(')') if squareOpenCount or curlyOpenCount or parenOpenCount: # If the brackets were not properly closed or the statement was # missing a separator, skip the rest of the var block. if not self.balance_pairs(squareOpenCount, curlyOpenCount, parenOpenCount): return False return True def var_block(self, blockMatch): """ Parse a var block, return a tuple (haveLine, isSingleVar), where haveLine indicates whether self.line is the next line to be parsed. """ # Keep track of whether this var block has multiple declarations isSingleVar = True # Keep track of the indent of the var keyword to compare with following lines self.varIndent = blockMatch.group('indent') # Keep track of how far the first variable name is indented to make sure # following lines line up with that self.identifierIndent = self.varIndent + blockMatch.group('var') # Check the expression to see if we have any open [ or { or /* self.get_expression(blockMatch) if not self.pairs_balanced(blockMatch): return (False, False) separator = '' if self.expression: match = self.SEPARATOR_RE.match(self.expression) if not match: self.error('missing statement separator', type=self.ERROR_TYPE_ILLEGAL) else: separator = match.group('separator') elif blockMatch.group('separator'): separator = blockMatch.group('separator') # If the block has a semicolon, there should be no more lines in the block blockHasSemicolon = separator == ';' # We may not catch an error till after the line that is wrong, so keep # the most recent declaration and its line number. lastBlockLine = self.line lastBlockLineNum = self.lineNum # Now construct an RE that will match any lines indented at least as much # as the var keyword that started the block. blockRE = re.compile(self.VAR_BLOCK_RE_TEMPLATE % len(self.identifierIndent)) while self.next_statement(expect_line=not blockHasSemicolon): if not self.is_statement(): continue # Is the line indented at least as much as the var keyword that started the block? match = blockRE.match(self.line) if match: if self.is_expression(): lastBlockLine = self.line lastBlockLineNum = self.lineNum # If the line is indented farther than the first identifier in the block, # it is considered a formatting error. if match.group('indent') and not match.group('indented_expression'): self.error('incorrect indentation') self.get_expression(match) if not self.pairs_balanced(match): return (False, isSingleVar) if self.expression: separatorMatch = self.SEPARATOR_RE.match(self.expression) if separatorMatch is None: # If the assignment does not have a separator, it's an error self.error('missing statement separator', type=self.ERROR_TYPE_ILLEGAL) else: separator = separatorMatch.group('separator') if blockHasSemicolon: # If the block already has a semicolon, we have an accidental global declaration self.error('accidental global variable', type=self.ERROR_TYPE_ILLEGAL) elif (separator == ';'): blockHasSemicolon = True elif match.group('separator'): separator = match.group('separator') isSingleVar = False else: # If the line is a control statement of some kind, then it should not be indented this far. self.error('statement should be outdented from preceding var block') return (True, False) else: # If the line does not match, it is not an assignment or is outdented from the block. # In either case, the block is considered closed. If the most recent separator was not ';', # the block was not properly terminated. if separator != ';': self.error('unterminated var block', lineNum=lastBlockLineNum, line=lastBlockLine, type=self.ERROR_TYPE_ILLEGAL) return (True, isSingleVar) def check_var_blocks(self): lastStatementWasVar = False lastVarWasSingle = False haveLine = True while True: if not haveLine: haveLine = self.next_statement() if not self.is_statement(): haveLine = False continue match = self.VAR_BLOCK_START_RE.match(self.line) if match is None: lastStatementWasVar = False haveLine = False continue # It might be a function definition, in which case we continue expression = match.group('expression') if expression: functionMatch = self.FUNCTION_RE.match(expression) if functionMatch: lastStatementWasVar = False haveLine = False continue # Now we have the start of a variable block if self.verbose: print u'%d: VAR BLOCK' % self.lineNum varLineNum = self.lineNum varLine = self.line haveLine, isSingleVar = self.var_block(match) if self.verbose: print u'%d: END VAR BLOCK:' % self.lineNum, if isSingleVar: print u'SINGLE' else: print u'MULTIPLE' if lastStatementWasVar and self.varDeclarations != self.VAR_DECLARATIONS_NONE: if (self.varDeclarations == self.VAR_DECLARATIONS_SINGLE and lastVarWasSingle and isSingleVar) or \ (self.varDeclarations == self.VAR_DECLARATIONS_STRICT and (lastVarWasSingle or isSingleVar)): self.error('consecutive var declarations', lineNum=varLineNum, line=varLine) lastStatementWasVar = True lastVarWasSingle = isSingleVar def run_file_checks(self): for check in self.fileChecklist: self.sourcefile.seek(0) self.lineNum = 0 if self.verbose: print u'%s: %s' % (check['title'], self.sourcefile.name) check['action']() def lint(self, filesToCheck): # Recursively walk any directories and eliminate duplicates self.filesToCheck = [] for filename in filesToCheck: filename = unicode(filename, 'utf-8') fullpath = os.path.join(self.basedir, filename) if fullpath not in self.filesToCheck: if os.path.isdir(fullpath): for root, dirs, files in os.walk(fullpath): for skipDir in self.DIRS_TO_SKIP: if skipDir in dirs: dirs.remove(skipDir) for filename in files: if not filename.endswith('.j'): continue fullpath = os.path.join(root, filename) if fullpath not in self.filesToCheck: self.filesToCheck.append(fullpath) else: self.filesToCheck.append(fullpath) for filename in self.filesToCheck: try: with open(filename) as self.sourcefile: self.filename = relative_path(self.basedir, filename) self.run_file_checks() except IOError: self.lineNum = 0 self.line = None self.error('file not found', type=self.ERROR_TYPE_ILLEGAL) except StopIteration: if self.verbose: print u'EOF\n' pass def lint_text(self, text, filename=""): self.filename = filename self.filesToCheck = [] try: self.sourcefile = cStringIO.StringIO(text) self.run_file_checks() except StopIteration: if self.verbose: print u'EOF\n' pass return self.errors def count_files_checked(self): return len(self.filesToCheck) def error(self, message, **kwargs): info = { 'filename': self.filename, 'message': message, 'type': kwargs.get('type', self.ERROR_TYPE_WARNING) } line = kwargs.get('line', self.line) lineNum = kwargs.get('lineNum', self.lineNum) if line and lineNum: info['line'] = tabs2spaces(line) info['lineNum'] = lineNum positions = kwargs.get('positions') if positions: info['positions'] = positions self.errors.append(info) if self.filename not in self.errorFiles: self.errorFiles.append(self.filename) def has_errors(self): return len(self.errors) != 0 def print_errors(self, format='text'): if not self.errors: return if format == 'text': self.print_text_errors() elif format == 'html': self.print_textmate_html_errors() elif format == 'tooltip': self.print_tooltip_errors() def print_text_errors(self): sys.stdout.write('%d error' % len(self.errors)) if len(self.errors) > 1: sys.stdout.write('s') if len(self.filesToCheck) == 1: template = self.TEXT_ERROR_SINGLE_FILE_TEMPLATE else: sys.stdout.write(' in %d files' % len(self.errorFiles)) template = self.TEXT_ERROR_MULTI_FILE_TEMPLATE sys.stdout.write(':\n\n') for error in self.errors: if 'lineNum' in error and 'line' in error: sys.stdout.write(template.substitute(error).encode('utf-8')) if error.get('positions'): markers = ' ' * len(error['line']) for position in error['positions']: markers = markers[:position] + '^' + markers[position + 1:] # Add a space at the beginning of the markers to account for the '+' at the beginning # of the source line. sys.stdout.write(' %s\n' % markers) else: sys.stdout.write('%s: %s.\n' % (error['filename'], error['message'])) sys.stdout.write('\n') def print_textmate_html_errors(self): html = """ Cappuccino Lint Report """ html += '

Results: %d error' % len(self.errors) if len(self.errors) > 1: html += 's' if len(self.filesToCheck) > 1: html += ' in %d files' % len(self.errorFiles) html += '

' for error in self.errors: message = cgi.escape(error['message']) if len(self.filesToCheck) > 1: filename = cgi.escape(error['filename']) + ':' else: filename = '' html += '

' if 'line' in error and 'lineNum' in error: filepath = cgi.escape(os.path.join(self.basedir, error['filename'])) lineNum = error['lineNum'] line = error['line'] positions = error.get('positions') firstPos = -1 source = '' if positions: firstPos = positions[0] + 1 lastPos = 0 for pos in error.get('positions'): if pos < len(line): charToHighlight = line[pos] else: charToHighlight = '' source += '%s%s' % (cgi.escape(line[lastPos:pos]), cgi.escape(charToHighlight)) lastPos = pos + 1 if lastPos <= len(line): source += cgi.escape(line[lastPos:]) else: source = line link = '' % (filepath, lineNum, firstPos) if len(self.filesToCheck) > 1: errorMsg = '%s%d: %s' % (filename, lineNum, message) else: errorMsg = '%d: %s' % (lineNum, message) html += '%(link)s%(errorMsg)s

\n

%(link)s%(source)s

\n' % {'link': link, 'errorMsg': errorMsg, 'source': source} else: html += '%s%s

\n' % (filename, message) html += """ """ exit_show_html(html) class MiscTest(unittest.TestCase): def test_string_replacer(self): self.assertEquals(string_replacer("x = 'hello';"), "x = '';") self.assertEquals(string_replacer("x = '\\' hello';"), "x = '';") self.assertEquals(string_replacer("x = '\\\\';"), "x = '';") self.assertEquals(string_replacer("""x = '"string in string"';"""), "x = '';") self.assertEquals(string_replacer('x = "hello";'), 'x = "";') self.assertEquals(string_replacer('x = "\\" hello";'), 'x = "";') self.assertEquals(string_replacer('x = "\\\\";'), 'x = "";') self.assertEquals(string_replacer('''x = "'";'''), 'x = "";') class LintCheckerTest(unittest.TestCase): def test_exponential_notation(self): """Test that exponential notation such as 1.1e-6 doesn't cause a warning about missing whitespace.""" # This should not report "binary operator without surrounding spaces". self.assertEquals(LintChecker().lint_text("a = 2.1e-6;"), []) self.assertEquals(LintChecker().lint_text("a = 2.1e+6;"), []) self.assertEquals(LintChecker().lint_text("a = 2e-0;"), []) self.assertEquals(LintChecker().lint_text("a = 2e+0;"), []) # But this should. self.assertEquals(LintChecker().lint_text("a = 1.1e-6+2e2;"), [{'positions': [6], 'filename': '', 'lineNum': 1, 'message': 'binary operator without surrounding spaces', 'type': 2, 'line': u'a = 1.1e-6+2e2;'}]) def test_function_types(self): """Test that function definitions like function(/*CPString*/key) don't cause warnings about surrounding spaces.""" # This should not report "binary operator without surrounding spaces". self.assertEquals(LintChecker().lint_text("var resolveMultipleValues = function(/*CPString*/key, /*CPDictionary*/bindings, /*GSBindingOperationKind*/operation)"), []) def test_unary_plus(self): """Test that = +, like in `x = +y;`, doesn't cause a warning.""" # + converts number in a string to a number. self.assertEquals(LintChecker().lint_text("var y = +x;"), []) def test_string_escaping(self): """Test that string literals are not parsed as syntax, even when they end with a double backslash.""" self.assertEquals(LintChecker().lint_text('var x = "(\\\\";'), []) if __name__ == '__main__': usage = 'usage: %prog [options] [file ... | -]' parser = OptionParser(usage=usage, version='1.02') parser.add_option('-f', '--format', action='store', type='string', dest='format', default='text', help='the format to use for the report: text (default) or html (HTML in which errors can be clicked on to view in TextMate)') parser.add_option('-b', '--basedir', action='store', type='string', dest='basedir', help='the base directory relative to which filenames are resolved, defaults to the current working directory') parser.add_option('-d', '--var-declarations', action='store', type='string', dest='var_declarations', default='single', help='set the policy for flagging consecutive var declarations (%s)' % ', '.join(LintChecker.VAR_DECLARATIONS)) parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False, help='show what lint is doing') parser.add_option('-q', '--quiet', action='store_true', dest='quiet', default=False, help='do not display errors, only return an exit code') (options, args) = parser.parse_args() if options.var_declarations not in LintChecker.VAR_DECLARATIONS: parser.error('--var-declarations must be one of [' + ', '.join(LintChecker.VAR_DECLARATIONS) + ']') if options.verbose and options.quiet: parser.error('options -v/--verbose and -q/--quiet are mutually exclusive') options.format = options.format.lower() if not options.format in LintChecker.ERROR_FORMATS: parser.error('format must be one of ' + '/'.join(LintChecker.ERROR_FORMATS)) if options.format == 'html' and not within_textmate(): parser.error('html format can only be used within TextMate.') if options.basedir: basedir = options.basedir if basedir[-1] == '/': basedir = basedir[:-1] else: basedir = os.getcwd() # We accept a list of filenames (relative to the cwd) either from the command line or from stdin filenames = args if args and args[0] == '-': filenames = [name.rstrip() for name in sys.stdin.readlines()] if not filenames: print usage.replace('%prog', os.path.basename(sys.argv[0])) sys.exit(0) checker = LintChecker(basedir=basedir, view=None, var_declarations=LintChecker.VAR_DECLARATIONS.index(options.var_declarations), verbose=options.verbose) pathsToCheck = [] for filename in filenames: filename = filename.strip('"\'') path = os.path.join(basedir, filename) if (os.path.isdir(path) and not path.endswith('Frameworks')) or filename.endswith('.j'): pathsToCheck.append(relative_path(basedir, filename)) if len(pathsToCheck) == 0: if within_textmate(): exit_show_tooltip('No Objective-J files found.') sys.exit(0) checker.lint(pathsToCheck) if checker.has_errors(): if not options.quiet: checker.print_errors(options.format) sys.exit(1) else: if within_textmate(): exit_show_tooltip('Everything looks clean.') sys.exit(0)