From 092cb01bb30f3dd1fd31aebc6a8fc776567854e3 Mon Sep 17 00:00:00 2001
From: Aparajita Fishman
Date: Fri, 20 Apr 2012 11:57:18 +0200
Subject: [PATCH] Added capp_lint to Tools
---
Tools/capp_lint/README | 7 +
Tools/capp_lint/capp_lint | 1012 +++++++++++++++++++++++++++++++++++
Tools/capp_lint/capp_lint.1 | 181 +++++++
Tools/capp_lint/install.sh | 15 +
Tools/capp_lint/pre-commit | 107 ++++
5 files changed, 1322 insertions(+)
create mode 100644 Tools/capp_lint/README
create mode 100755 Tools/capp_lint/capp_lint
create mode 100644 Tools/capp_lint/capp_lint.1
create mode 100755 Tools/capp_lint/install.sh
create mode 100644 Tools/capp_lint/pre-commit
diff --git a/Tools/capp_lint/README b/Tools/capp_lint/README
new file mode 100644
index 000000000..47a2ca97a
--- /dev/null
+++ b/Tools/capp_lint/README
@@ -0,0 +1,7 @@
+capp_lint is a formatting checker for Objective-J. It performs a number of checks that catch
+common formatting mistakes, as defined in the Cappuccino coding style guidelines:
+
+http://cappuccino.org/contribute/coding-style.php
+
+To install capp_lint and its man page, execute install.sh in Terminal. You must have sudo access
+to run the install script.
diff --git a/Tools/capp_lint/capp_lint b/Tools/capp_lint/capp_lint
new file mode 100755
index 000000000..4801b81db
--- /dev/null
+++ b/Tools/capp_lint/capp_lint
@@ -0,0 +1,1012 @@
+#!/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
+
+
+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
+
+
+class LintChecker(object):
+
+ 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*|//.*)$')
+ 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*\{?')
+ STRING_LITERAL_RE = re.compile(ur'(?]*\)\s*\w+|[a-zA-Z_$]\w*(\+\+|--)|([ -+*/%^&|<>!]=?|&&|\|\||<<|>>>|={1,3}|!==?)\s*[-+][\w(\[])'), 'pass': False},
+
+ 'preprocess': STD_IGNORES,
+ 'regex': re.compile(ur'(?<=[\w)\]"\']|([ ]))([-+*/%^]|&&?|\|\|?|<<|>>>?)(?=[\w({\["\']|(?(1)\b\b|[ ]))'),
+ 'error': 'binary operator without surrounding spaces',
+ 'showPositionForGroup': 2,
+ 'type': ERROR_TYPE_WARNING
+ },
+ {
+ # Filter out @import statements, method declarations
+ 'filter': {'regex': re.compile(ur'^(@import\b|\s*' + METHOD_RE + ')'), 'pass': False},
+
+ 'preprocess': STD_IGNORES,
+ 'regex': re.compile(ur'(?:[-*/%^&|<>!]=?|&&|\|\||<<|>>>|={1,3}|!==?)\s*(?>>?=)(?=[\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, 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 not self.view.settings().get(option, default):
+ continue
+
+ line = self.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)
+
+ regex = check.get('regex')
+
+ if not regex:
+ continue
+
+ match = regex.search(line)
+
+ if not match:
+ continue
+
+ positions = []
+ group = check.get('showPositionForGroup')
+
+ if (check.get('id') == 'tabs'):
+ line = tabs2spaces(line, positions=positions)
+ elif group is not None:
+ line = tabs2spaces(line)
+
+ for match in regex.finditer(line):
+ if group > 0:
+ positions.append(match.start(group))
+ 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
+
+ self.error(check['error'], line=line, positions=positions, type=check['type'])
+
+ def next_statement(self, expect_line=False, check_line=True):
+ try:
+ while True:
+ raw_line = self.sourcefile.next()[:-1] # strip EOL
+
+ 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 = self.STRING_LITERAL_RE.sub(self.EMPTY_SELF_STRING_LITERAL_FUNCTION, 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
+
+ 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)
+
+
+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, 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)
diff --git a/Tools/capp_lint/capp_lint.1 b/Tools/capp_lint/capp_lint.1
new file mode 100644
index 000000000..e0e6c0b3c
--- /dev/null
+++ b/Tools/capp_lint/capp_lint.1
@@ -0,0 +1,181 @@
+.Dd April 20, 2012
+.Os "Cappuccino"
+.Dt CAPP_LINT 1 "PRM"
+.\"-----------------------------------------------------------------------------------------
+.Sh NAME
+.\"-----------------------------------------------------------------------------------------
+.Nm capp_lint
+.Nd check formatting of Objective-J files
+.\"-----------------------------------------------------------------------------------------
+.Sh SYNOPSIS
+.\"-----------------------------------------------------------------------------------------
+.Nm
+.Op options
+.Op Ar
+.Nm
+.Op --version | -h | --help
+.\"-----------------------------------------------------------------------------------------
+.Sh "DESCRIPTION"
+.\"-----------------------------------------------------------------------------------------
+.Nm
+is a formatting checker for Objective-J. It performs a number of checks that catch
+common formatting mistakes, as defined in the Cappuccino coding style guidelines:
+.Pp
+http://cappuccino.org/contribute/coding-style.php
+.Pp
+.Nm
+operates on a list of files and/or directory. You may pass one or more paths (relative to the
+current working directory) as arguments to
+.Nm ,
+or if the first path is "-", the
+list of paths is taken from stdin, in which case everything after the "-" is ignored.
+.Pp
+If a path is a directory, the directory is recursively scanned for *.j files to check. Symbolic links
+are not followed.
+.Pp
+The following options are available:
+.Bl -tag -width 4n
+.It Fl b, \-basedir=BASEDIR
+The base directory relative to which filenames will be resolved. If not passed,
+it defaults to the current working directory.
+.It Fl d, \-var-declarations=[none|single|strict]
+Determines the policy in flagging consecutive var declarations. The choices are:
+.Bl -tag -indent -width 8n
+.It none
+No checking is done.
+.It single
+If two var blocks appear consecutively and one of them is
+a single variable, the second var block will be flagged.
+.It strict
+Any consecutive var declarations will be flagged.
+.El
+.Pp
+Given the following code:
+.Pp
+ var foo = "bar",
+ bar = "foo";
+
+ var bow = "foobar",
+ wow = "woof";
+.Pp
+The
+.Ar none
+policy will obviously not flag this. The
+.Ar single
+policy will not flag
+this because both blocks contain more than one declaration. The
+.Ar strict
+policy will flag this. Given the following code:
+.Pp
+ var foo = "bar",
+ bar = "foo";
+
+ var bow = "foobar";
+.Pp
+The
+.Ar single
+policy will flag this because the second block is a single declaration.
+.It Fl f, \-format=[text|html]
+Determines the format of the error output. See the OUTPUT section below for more information
+on the different formats.
+.It Fl q, \-quiet
+Normally
+.Nm
+will print a list of the errors it finds. If this option is passed,
+the error list is suppressed. This option is mutually exclusive with the -v option.
+.It Fl v, \-verbose
+Displays each line as it is processed by
+.Nm
+with a line number, along with some information about syntactic information it finds.
+.It Fl \-version
+Displays the current version and exits.
+.It Fl h, \-help
+Shows a synopsis of usage and options and exits.
+.El
+.Ss Errors flagged
+The following formatting errors are flagged by
+.Nm :
+.Bl -bullet -width 0n
+.It
+Lines which contain one or more tabs.
+.It
+Lines which contain non-ASCII characters not within quoted strings.
+.It
+Lines which contain malformed UTF-8.
+.It
+Missing space between control statements and their opening parentheses.
+.It
+Trailing whitespace.
+.It
+Binary operators without surrounding space.
+.It
+Assignment operators without surrounding space.
+.It
+Comparison operators without surrounding space.
+.It
+Useless unary + operators.
+.It
+Extra or missing space in a method declaration.
+.It
+Superflous function names.
+.It
+Unterminated variable blocks.
+.It
+Consecutive var statements.
+.It
+Incorrect indentation in a var block.
+.It
+Missing statement separators in a var block.
+.It
+Inadvertent global variables in a var block.
+.It
+Unbalanced [, { or ( in a variable block.
+.It
+Opening braces on the same line as a statement.
+.It
+Code following a var block that is not outdented from the var block.
+.El
+.\"-----------------------------------------------------------------------------------------
+.Sh EXIT STATUS
+.\"-----------------------------------------------------------------------------------------
+.Nm
+exits with a return status of one if it finds any errors. If no errors are found,
+it exits with a return status of zero.
+.\"-----------------------------------------------------------------------------------------
+.Sh OUTPUT
+.\"-----------------------------------------------------------------------------------------
+For each error that
+.Nm
+finds, if the -q/--quiet option was not passed,
+.Nm
+will output an error like the following if the error format is text:
+.Pp
+Foo.j:482: assignment operator without surrounding spaces.
+.br
++ for (var i=0, count=[_observationProxies count]; i&2
+ echo 'pre-commit check failure' 1>&2
+ echo '-------------------------' 1>&2
+ echo "$@" 1>&2
+
+ if [ -f /tmp/capp_lint.tmp ]; then
+ rm -f /tmp/capp_lint.tmp
+ fi
+
+ exit 1
+}
+
+#-----------------------------------------------------------------------------
+# Check for committer identity.
+advice='
+Use the commands:
+
+ git config --global user.name '\''Your Name'\''
+ git config --global user.email '\''you@yourdomain.com'\''
+
+to introduce yourself to Git before committing.'
+
+# Ensure name and email are available.
+git config --get user.name > /dev/null &&
+git config --get user.email > /dev/null ||
+die 'Identity not configured!' "$advice"
+
+# Validate the name and email.
+git config --get user.name | grep ' ' > /dev/null ||
+die 'Please set user.name to your Real Name (with a space), not a userid.' "$advice"
+git config --get user.email | grep '^[^@]*@[^@]*$' > /dev/null ||
+die 'Please set user.email to an email address (userid@validdomain.com).' "$advice"
+
+#-----------------------------------------------------------------------------
+# Check content that will be added by this commit.
+
+if git rev-parse --verify -q HEAD > /dev/null; then
+ against=HEAD
+else
+ # Initial commit: diff against an empty tree object
+ against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
+fi
+
+# Disallow non-ascii file names. The printable range starts at the
+# space character and ends with tilde.
+if test "$(git diff --cached --name-only --diff-filter=A -z $against |
+ LC_ALL=C tr -d '[ -~]\0')"; then
+ die 'Non-ascii file names may not be added:
+'"$(git diff --cached --name-only --diff-filter=A $against)"
+fi
+
+# Check for trailing whitespaces, indents that uses a space before a tab and leftover conflict markers
+if test "$(git diff-index --check --cached $against --)"; then
+ die 'The following whitespace errors or unresolved conflicts have been found:
+'"$(git diff-index --check --cached $against --)"
+fi
+
+#-----------------------------------------------------------------------------
+# Check for inadvertent globals and dangling variable declarations.
+
+fileCount=`git diff --cached --name-only --diff-filter=AM $against | wc -l`
+
+if [[ fileCount -ne "0" ]]; then
+ lintPath=''
+
+ if [[ `which capp_lint.py` ]]; then
+ lintPath="`which capp_lint.py`"
+ fi
+
+ if [[ -z "$lintPath" && `which capp_lint` ]]; then
+ lintPath="`which capp_lint`"
+ fi
+
+ if [[ -z "$lintPath" ]]; then
+ path=`dirname $0`/capp_lint.py
+
+ if [[ -x "$path" ]]; then
+ lintPath="$path"
+ fi
+ fi
+
+ if [[ -z "$lintPath" ]]; then
+ die 'Could not find capp_lint or capp_lint.py in your PATH or in .git/hooks.'
+ fi
+
+ git diff --cached --name-only --diff-filter=AM $against | "$lintPath" - > /tmp/capp_lint.tmp
+
+ if [ -s /tmp/capp_lint.tmp ]; then
+ die 'The following lint errors were found:
+
+ '"$(cat /tmp/capp_lint.tmp)"
+ else
+ rm -f /tmp/capp_lint.tmp
+ fi
+fi
+
+exit 0