diff --git a/tests/rules/test_indentation.py b/tests/rules/test_indentation.py index 2beeb58..a3b5ad9 100644 --- a/tests/rules/test_indentation.py +++ b/tests/rules/test_indentation.py @@ -496,6 +496,87 @@ class IndentationTestCase(RuleTestCase): ' date: 1991\n' '...\n', conf) + def test_consistent(self): + conf = ('indentation: {spaces: consistent,\n' + ' indent-sequences: whatever}\n' + 'document-start: disable\n') + self.check('---\n' + 'object:\n' + ' k1:\n' + ' - a\n' + ' - b\n' + ' k2: v2\n' + ' k3:\n' + ' - name: Unix\n' + ' date: 1969\n' + ' - name: Linux\n' + ' date: 1991\n' + '...\n', conf) + self.check('---\n' + 'object:\n' + ' k1:\n' + ' - a\n' + ' - b\n' + ' k2: v2\n' + ' k3:\n' + ' - name: Unix\n' + ' date: 1969\n' + ' - name: Linux\n' + ' date: 1991\n' + '...\n', conf) + self.check('---\n' + 'object:\n' + ' k1:\n' + ' - a\n' + ' - b\n' + ' k2: v2\n' + ' k3:\n' + ' - name: Unix\n' + ' date: 1969\n' + ' - name: Linux\n' + ' date: 1991\n' + '...\n', conf) + self.check('first is not indented:\n' + ' value is indented\n', conf) + self.check('first is not indented:\n' + ' value:\n' + ' is indented\n', conf) + self.check('- first is already indented:\n' + ' value is indented too\n', conf) + self.check('- first is already indented:\n' + ' value:\n' + ' is indented too\n', conf) + self.check('- first is already indented:\n' + ' value:\n' + ' is indented too\n', conf, problem=(3, 14)) + self.check('---\n' + 'list one:\n' + ' - 1\n' + ' - 2\n' + ' - 3\n' + 'list two:\n' + ' - a\n' + ' - b\n' + ' - c\n', conf, problem=(7, 5)) + self.check('---\n' + 'list one:\n' + '- 1\n' + '- 2\n' + '- 3\n' + 'list two:\n' + ' - a\n' + ' - b\n' + ' - c\n', conf) + self.check('---\n' + 'list one:\n' + ' - 1\n' + ' - 2\n' + ' - 3\n' + 'list two:\n' + '- a\n' + '- b\n' + '- c\n', conf) + def test_indent_sequences_whatever(self): conf = 'indentation: {spaces: 4, indent-sequences: whatever}' self.check('---\n' @@ -1528,3 +1609,39 @@ class ScalarIndentationTestCase(RuleTestCase): ' paragraph gap, \\n and\n' ' spaces.\n', conf, problem1=(3, 6), problem2=(5, 7), problem3=(6, 8)) + + def test_consistent(self): + conf = ('indentation: {spaces: consistent,\n' + ' check-multi-line-strings: yes}\n' + 'document-start: disable\n') + self.check('multi\n' + 'line\n', conf) + self.check('multi\n' + ' line\n', conf, problem=(2, 2)) + self.check('- multi\n' + ' line\n', conf) + self.check('- multi\n' + ' line\n', conf, problem=(2, 4)) + self.check('a key: multi\n' + ' line\n', conf, problem=(2, 3)) + self.check('a key: multi\n' + ' line\n', conf, problem=(2, 9)) + self.check('a key:\n' + ' multi\n' + ' line\n', conf, problem=(3, 4)) + self.check('- C code: void main() {\n' + ' printf("foo");\n' + ' }\n', conf, problem=(2, 15)) + self.check('- C code:\n' + ' void main() {\n' + ' printf("foo");\n' + ' }\n', conf, problem=(3, 9)) + self.check('>\n' + ' multi\n' + ' line\n', conf) + self.check('>\n' + ' multi\n' + ' line\n', conf) + self.check('>\n' + ' multi\n' + ' line\n', conf, problem=(3, 7)) diff --git a/yamllint/rules/indentation.py b/yamllint/rules/indentation.py index 8ff74fd..3df1cc6 100644 --- a/yamllint/rules/indentation.py +++ b/yamllint/rules/indentation.py @@ -19,7 +19,10 @@ Use this rule to control the indentation. .. rubric:: Options -* ``spaces`` defines the number of spaces that represent an indentation level. +* ``spaces`` defines the indentation width, in spaces. Set either to an integer + (e.g. ``2`` or ``4``, representing the number of spaces in an indentation + level) or to ``consistent`` to allow any number, as long as it remains the + same within the file. * ``indent-sequences`` defines whether block sequences should be indented or not (when in a mapping, this indentation is not mandatory -- some people perceive the ``-`` as part of the indentation). Possible values: ``yes``, @@ -73,6 +76,28 @@ Use this rule to control the indentation. - haystack: needle +#. With ``indentation: {spaces: consistent}`` + + the following code snippet would **PASS**: + :: + + history: + - name: Unix + date: 1969 + - name: Linux + date: 1991 + nest: + recurse: + - haystack: + needle + + the following code snippet would **FAIL**: + :: + + some: + Russian: + dolls + #. With ``indentation: {spaces: 2, indent-sequences: no}`` the following code snippet would **PASS**: @@ -150,7 +175,7 @@ from yamllint.rules.common import is_explicit_key, get_real_end_line ID = 'indentation' TYPE = 'token' -CONF = {'spaces': int, +CONF = {'spaces': (int, 'consistent'), 'indent-sequences': (True, False, 'whatever'), 'check-multi-line-strings': bool} @@ -174,45 +199,53 @@ def check_scalar_indentation(conf, token, context): if token.start_mark.line == token.end_mark.line: return - if token.plain: - expected_indent = token.start_mark.column - elif token.style in ('"', "'"): - expected_indent = token.start_mark.column + 1 - elif token.style in ('>', '|'): - if context['stack'][-1].type == B_SEQ: - # - > - # multi - # line - expected_indent = token.start_mark.column + conf['spaces'] - elif context['stack'][-1].type == KEY: - assert context['stack'][-1].explicit_key - # - ? > - # multi-line - # key - # : > - # multi-line - # value - expected_indent = token.start_mark.column + conf['spaces'] - elif context['stack'][-1].type == VAL: - if token.start_mark.line + 1 > context['cur_line']: - # - key: - # > - # multi - # line - expected_indent = context['stack'][-1].indent + conf['spaces'] - elif context['stack'][-2].explicit_key: - # - ? key + def compute_expected_indent(found_indent): + def detect_indent(base_indent): + if type(context['spaces']) is not int: + context['spaces'] = found_indent - base_indent + return base_indent + context['spaces'] + + if token.plain: + return token.start_mark.column + elif token.style in ('"', "'"): + return token.start_mark.column + 1 + elif token.style in ('>', '|'): + if context['stack'][-1].type == B_SEQ: + # - > + # multi + # line + return detect_indent(token.start_mark.column) + elif context['stack'][-1].type == KEY: + assert context['stack'][-1].explicit_key + # - ? > + # multi-line + # key # : > # multi-line # value - expected_indent = token.start_mark.column + conf['spaces'] + return detect_indent(token.start_mark.column) + elif context['stack'][-1].type == VAL: + if token.start_mark.line + 1 > context['cur_line']: + # - key: + # > + # multi + # line + return detect_indent(context['stack'][-1].indent) + elif context['stack'][-2].explicit_key: + # - ? key + # : > + # multi-line + # value + return detect_indent(token.start_mark.column) + else: + # - key: > + # multi + # line + return detect_indent(context['stack'][-2].indent) else: - # - key: > - # multi - # line - expected_indent = context['stack'][-2].indent + conf['spaces'] - else: - expected_indent = context['stack'][-1].indent + conf['spaces'] + return detect_indent(context['stack'][-1].indent) + + expected_indent = None line_no = token.start_mark.line + 1 @@ -230,6 +263,9 @@ def check_scalar_indentation(conf, token, context): if token.start_mark.buffer[line_start + indent] == '\n': continue + if expected_indent is None: + expected_indent = compute_expected_indent(indent) + if indent != expected_indent: yield LintProblem(line_no, indent + 1, 'wrong indentation: expected %d but found %d' % @@ -240,6 +276,7 @@ def check(conf, token, prev, next, nextnext, context): if 'stack' not in context: context['stack'] = [Parent(ROOT, 0)] context['cur_line'] = -1 + context['spaces'] = conf['spaces'] # Step 1: Lint @@ -250,6 +287,12 @@ def check(conf, token, prev, next, nextnext, context): first_in_line = (is_visible and token.start_mark.line + 1 > context['cur_line']) + def detect_indent(next): + if type(context['spaces']) is not int: + context['spaces'] = (next.start_mark.column - + context['stack'][-1].indent) + return context['spaces'] + if first_in_line: found_indentation = token.start_mark.column expected = context['stack'][-1].indent @@ -260,7 +303,7 @@ def check(conf, token, prev, next, nextnext, context): elif (context['stack'][-1].type == KEY and context['stack'][-1].explicit_key and not isinstance(token, yaml.ValueToken)): - expected += conf['spaces'] + expected += detect_indent(token) if found_indentation != expected: yield LintProblem(token.start_mark.line + 1, found_indentation + 1, @@ -294,7 +337,7 @@ def check(conf, token, prev, next, nextnext, context): # - ? # a # : 1 - indent = token.start_mark.column + conf['spaces'] + indent = token.start_mark.column + detect_indent(next) context['stack'].append(Parent(B_MAP, indent)) @@ -306,7 +349,7 @@ def check(conf, token, prev, next, nextnext, context): # - { # a: 1, b: 2 # } - indent = context['cur_line_indent'] + conf['spaces'] + indent = context['cur_line_indent'] + detect_indent(next) context['stack'].append(Parent(F_MAP, indent, line_indent=context['cur_line_indent'])) @@ -340,7 +383,7 @@ def check(conf, token, prev, next, nextnext, context): # - # key: # value - indent = token.start_mark.column + conf['spaces'] + indent = token.start_mark.column + detect_indent(next) context['stack'].append(Parent(B_ENT, indent)) @@ -352,7 +395,7 @@ def check(conf, token, prev, next, nextnext, context): # - [ # a, b # ] - indent = context['cur_line_indent'] + conf['spaces'] + indent = context['cur_line_indent'] + detect_indent(next) context['stack'].append(Parent(F_SEQ, indent, line_indent=context['cur_line_indent'])) @@ -390,7 +433,7 @@ def check(conf, token, prev, next, nextnext, context): # ? k # : # value - indent = context['stack'][-1].indent + conf['spaces'] + indent = context['stack'][-1].indent + detect_indent(next) elif next.start_mark.line == prev.start_mark.line: # k: value indent = next.start_mark.column @@ -404,7 +447,7 @@ def check(conf, token, prev, next, nextnext, context): if conf['indent-sequences'] is False: indent = context['stack'][-1].indent elif conf['indent-sequences'] is True: - indent = context['stack'][-1].indent + conf['spaces'] + indent = context['stack'][-1].indent + detect_indent(next) else: # 'whatever' if next.start_mark.column == context['stack'][-1].indent: # key: @@ -415,11 +458,12 @@ def check(conf, token, prev, next, nextnext, context): # key: # - e1 # - e2 - indent = context['stack'][-1].indent + conf['spaces'] + indent = (context['stack'][-1].indent + + detect_indent(next)) else: # k: # value - indent = context['stack'][-1].indent + conf['spaces'] + indent = context['stack'][-1].indent + detect_indent(next) context['stack'].append(Parent(VAL, indent))