diff options
author | ZyX <kp-pav@yandex.ru> | 2015-12-09 19:53:46 +0300 |
---|---|---|
committer | ZyX <kp-pav@yandex.ru> | 2015-12-11 19:08:46 +0300 |
commit | e7966322409b2a9f7d97366479e3053a4361d386 (patch) | |
tree | 7e6e6725a9b44f9ed8325f32004d5f8c1266539f /clint.py | |
parent | 74c960007fb06a4a37fd2cd457f1ef40d86ec55b (diff) | |
download | rneovim-e7966322409b2a9f7d97366479e3053a4361d386.tar.gz rneovim-e7966322409b2a9f7d97366479e3053a4361d386.tar.bz2 rneovim-e7966322409b2a9f7d97366479e3053a4361d386.zip |
clint: Check indentation and alignment inside expressions
Diffstat (limited to 'clint.py')
-rwxr-xr-x | clint.py | 193 |
1 files changed, 174 insertions, 19 deletions
@@ -198,6 +198,8 @@ _ERROR_CATEGORIES = [ 'runtime/printf', 'runtime/printf_format', 'runtime/threadsafe_fn', + 'syntax/parenthesis', + 'whitespace/alignment', 'whitespace/blank_line', 'whitespace/braces', 'whitespace/comma', @@ -213,7 +215,7 @@ _ERROR_CATEGORIES = [ 'whitespace/parens', 'whitespace/semicolon', 'whitespace/tab', - 'whitespace/todo' + 'whitespace/todo', ] # The default state of the category filter. This is overrided by the --filter= @@ -826,9 +828,9 @@ def Error(filename, linenum, category, confidence, message): _RE_PATTERN_CLEANSE_LINE_ESCAPES = re.compile( r'\\([abfnrtv?"\\\']|\d+|x[0-9a-fA-F]+)') # Matches strings. Escape codes should already be removed by ESCAPES. -_RE_PATTERN_CLEANSE_LINE_DOUBLE_QUOTES = re.compile(r'"[^"]*"') +_RE_PATTERN_CLEANSE_LINE_DOUBLE_QUOTES = re.compile(r'"([^"]*)"') # Matches characters. Escape codes should already be removed by ESCAPES. -_RE_PATTERN_CLEANSE_LINE_SINGLE_QUOTES = re.compile(r"'.'") +_RE_PATTERN_CLEANSE_LINE_SINGLE_QUOTES = re.compile(r"'(.)'") # Matches multi-line C++ comments. # This RE is a little bit more complicated than one might expect, because we # have to take care of space removals tools so we can handle comments inside @@ -923,12 +925,14 @@ def CleanseComments(line): class CleansedLines(object): - """Holds 4 copies of all lines with different preprocessing applied to them. + """Holds 5 copies of all lines with different preprocessing applied to them. 1) elided member contains lines without strings and comments, 2) lines member contains lines without comments, and 3) raw_lines member contains all the lines with multiline comments replaced. 4) init_lines member contains all the lines without processing. + 5) elided_with_space_strings is like elided, but with string literals + looking like `" "`. All these three members are of <type 'list'>, and of the same length. """ @@ -939,25 +943,30 @@ class CleansedLines(object): self.num_lines = len(lines) self.init_lines = init_lines self.lines_without_raw_strings = lines + self.elided_with_space_strings = [] for linenum in range(len(self.lines_without_raw_strings)): self.lines.append(CleanseComments( self.lines_without_raw_strings[linenum])) elided = self._CollapseStrings( self.lines_without_raw_strings[linenum]) self.elided.append(CleanseComments(elided)) + elided = CleanseComments(self._CollapseStrings( + self.lines_without_raw_strings[linenum], True)) + self.elided_with_space_strings.append(elided) def NumLines(self): """Returns the number of lines represented.""" return self.num_lines @staticmethod - def _CollapseStrings(elided): + def _CollapseStrings(elided, keep_spaces=False): """Collapses strings and chars on a line to simple "" or '' blocks. We nix strings first so we're not fooled by text like '"http://"' Args: elided: The line being processed. + keep_spaces: If true, collapse to Returns: The line with collapsed strings. @@ -966,12 +975,75 @@ class CleansedLines(object): # Remove escaped characters first to make quote/single quote # collapsing basic. Things that look like escaped characters # shouldn't occur outside of strings and chars. - elided = _RE_PATTERN_CLEANSE_LINE_ESCAPES.sub('', elided) - elided = _RE_PATTERN_CLEANSE_LINE_SINGLE_QUOTES.sub("''", elided) - elided = _RE_PATTERN_CLEANSE_LINE_DOUBLE_QUOTES.sub('""', elided) + elided = _RE_PATTERN_CLEANSE_LINE_ESCAPES.sub( + '' if not keep_spaces else lambda m: ' ' * len(m.group(0)), + elided) + elided = _RE_PATTERN_CLEANSE_LINE_SINGLE_QUOTES.sub( + "''" if not keep_spaces + else lambda m: "'" + (' ' * len(m.group(1))) + "'", + elided) + elided = _RE_PATTERN_CLEANSE_LINE_DOUBLE_QUOTES.sub( + '""' if not keep_spaces + else lambda m: '"' + (' ' * len(m.group(1))) + '"', + elided) return elided +BRACES = { + '(': ')', + '{': '}', + '[': ']', + # '<': '>', C++-specific pair removed +} + + +CLOSING_BRACES = dict(((v, k) for k, v in BRACES.items())) + + +def GetExprBracesPosition(clean_lines, linenum, pos): + """List positions of all kinds of braces + + If input points to ( or { or [ then function proceeds until finding the + position which closes it. + + Args: + clean_lines: A CleansedLines instance containing the file. + linenum: Current line number. + pos: A position on the line. + + Yields: + A tuple (linenum, pos, brace, depth) that points to each brace. + Additionally each new line (linenum, pos, 's', depth) is yielded, for each + line end (linenum, pos, 'e', depth) is yielded and at the very end it + yields (linenum, pos, None, None). + """ + depth = 0 + yielded_line_start = True + startpos = pos + while linenum < clean_lines.NumLines() - 1: + line = clean_lines.elided_with_space_strings[linenum] + if not line.startswith('#') or yielded_line_start: + # Ignore #ifdefs, but not if it is macros that are checked + for i, brace in enumerate(line[startpos:]): + pos = i + startpos + if brace != ' ' and not yielded_line_start: + yield (linenum, pos, 's', depth) + yielded_line_start = True + if brace in BRACES: + depth += 1 + yield (linenum, pos, brace, depth) + elif brace in CLOSING_BRACES: + yield (linenum, pos, brace, depth) + depth -= 1 + if depth == 0: + yield (linenum, pos, None, None) + return + yield (linenum, len(line) - 1, 'e', depth) + yielded_line_start = False + startpos = 0 + linenum += 1 + + def FindEndOfExpressionInLine(line, startpos, depth, startchar, endchar): """Find the position just after the matching endchar. @@ -997,9 +1069,9 @@ def FindEndOfExpressionInLine(line, startpos, depth, startchar, endchar): def CloseExpression(clean_lines, linenum, pos): - """If input points to ( or { or [ or <, finds the position that closes it. + """If input points to ( or { or [, finds the position that closes it. - If lines[linenum][pos] points to a '(' or '{' or '[' or '<', finds the + If lines[linenum][pos] points to a '(' or '{' or '[', finds the linenum/pos that correspond to the closing of the expression. Args: @@ -1016,16 +1088,9 @@ def CloseExpression(clean_lines, linenum, pos): line = clean_lines.elided[linenum] startchar = line[pos] - if startchar not in '({[<': + if startchar not in BRACES: return (line, clean_lines.NumLines(), -1) - if startchar == '(': - endchar = ')' - if startchar == '[': - endchar = ']' - if startchar == '{': - endchar = '}' - if startchar == '<': - endchar = '>' + endchar = BRACES[startchar] # Check first line (end_pos, num_open) = FindEndOfExpressionInLine( @@ -1987,6 +2052,92 @@ def FindPreviousMatchingAngleBracket(clean_lines, linenum, init_prefix): return False +def CheckExpressionAlignment(filename, clean_lines, linenum, error, startpos=0): + """Checks for the correctness of alignment inside expressions + + Args: + filename: The name of the current file. + clean_lines: A CleansedLines instance containing the file. + linenum: The number of the line to check. + error: The function to call with any errors found. + startpos: Position where to start searching for expression start. + """ + level_starts = {} + line = clean_lines.elided_with_space_strings[linenum] + prev_line_start = Search(r'\S', line).start() + depth_line_starts = {} + pos = min([ + idx + for idx in ( + line.find(k, startpos) + for k in BRACES + if k != '{' + ) + if idx >= 0 + ] + [len(line) + 1]) + if pos == len(line) + 1: + return + ignore_error_levels = set() + firstlinenum = linenum + for linenum, pos, brace, depth in GetExprBracesPosition( + clean_lines, linenum, pos + ): + line = clean_lines.elided_with_space_strings[linenum] + if depth is None: + if pos < len(line) - 1: + CheckExpressionAlignment(filename, clean_lines, linenum, error, + pos + 1) + return + elif depth <= 0: + error(filename, linenum, 'syntax/parenthesis', 4, + 'Unbalanced parenthesis') + return + if brace == 's': + assert firstlinenum != linenum + if level_starts[depth][1]: + if line[pos] == BRACES[depth_line_starts[depth][1]]: + if pos != depth_line_starts[depth][0]: + if depth not in ignore_error_levels: + error(filename, linenum, 'whitespace/indent', 2, + 'End of the inner expression should have ' + 'the same indent as start') + else: + if (pos != depth_line_starts[depth][0] + 4 + and not (depth_line_starts[depth][1] == '{' + and pos == depth_line_starts[depth][0] + 2)): + if depth not in ignore_error_levels: + error(filename, linenum, 'whitespace/indent', 2, + 'Inner expression indentation should be 4') + else: + if (pos != level_starts[depth][0] + 1 + + (level_starts[depth][2] == '{')): + if depth not in ignore_error_levels: + error(filename, linenum, 'whitespace/alignment', 2, + 'Inner expression should be aligned ' + 'as opening brace + 1 (+ 2 in case of {)') + prev_line_start = pos + elif brace == 'e': + pass + else: + opening = brace in BRACES + if opening: + # Only treat {} as part of the expression if it is preceded by + # "=" (brace initializer) or "(type)" (construct like (struct + # foo) { ... }). + if brace == '{' and not (Search( + r'(?:= *|\((?:struct )?\w+(\s*\[\w*\])?\)) *$', + line[:pos]) + ): + ignore_error_levels.add(depth) + line_ended_with_opening = ( + pos == len(line) - 2 * (line.endswith(' \\')) - 1) + level_starts[depth] = (pos, line_ended_with_opening, brace) + if line_ended_with_opening: + depth_line_starts[depth] = (prev_line_start, brace) + else: + del level_starts[depth] + + def CheckSpacing(filename, clean_lines, linenum, nesting_state, error): """Checks for the correctness of various spacing issues in the code. @@ -2256,6 +2407,10 @@ def CheckSpacing(filename, clean_lines, linenum, nesting_state, error): # Next we will look for issues with function calls. CheckSpacingForFunctionCall(filename, line, linenum, error) + # Check whether everything inside expressions is aligned correctly + if any((line.find(k) >= 0 for k in BRACES if k != '{')): + CheckExpressionAlignment(filename, clean_lines, linenum, error) + # Except after an opening paren, or after another opening brace (in case of # an initializer list, for instance), you should have spaces before your # braces. And since you should never have braces at the beginning of a line, |