diff --git a/tests/rules/test_indentation.py b/tests/rules/test_indentation.py index fc3ec53..ad8a5e6 100644 --- a/tests/rules/test_indentation.py +++ b/tests/rules/test_indentation.py @@ -488,3 +488,234 @@ class IndentationTestCase(RuleTestCase): ' :\n' ' value\n' '...\n', conf, problem1=(4, 10), problem2=(6, 8)) + + +class ScalarIndentationTestCase(RuleTestCase): + rule_id = 'indentation' + + def test_basics_plain(self): + conf = ('indentation: {spaces: 2}\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) + self.check('a key: multi\n' + ' line\n', conf, problem=(2, 9)) + self.check('a key:\n' + ' multi\n' + ' line\n', conf) + self.check('a key:\n' + ' multi\n' + ' line\n', conf, problem=(3, 4)) + + def test_basics_quoted(self): + conf = ('indentation: {spaces: 2}\n' + 'document-start: disable\n') + self.check('"multi\n' + ' line"\n', conf) + self.check('"multi\n' + 'line"\n', conf, problem=(2, 1)) + self.check('"multi\n' + ' line"\n', conf, problem=(2, 3)) + self.check('- "multi\n' + ' line"\n', conf) + self.check('- "multi\n' + ' line"\n', conf, problem=(2, 3)) + self.check('- "multi\n' + ' line"\n', conf, problem=(2, 5)) + self.check('a key: "multi\n' + ' line"\n', conf) + self.check('a key: "multi\n' + ' line"\n', conf, problem=(2, 8)) + self.check('a key: "multi\n' + ' line"\n', conf, problem=(2, 10)) + self.check('a key:\n' + ' "multi\n' + ' line"\n', conf) + self.check('a key:\n' + ' "multi\n' + ' line"\n', conf, problem=(3, 3)) + self.check('a key:\n' + ' "multi\n' + ' line"\n', conf, problem=(3, 5)) + + def test_basics_folded_style(self): + conf = ('indentation: {spaces: 2}\n' + 'document-start: disable\n') + self.check('>\n' + ' multi\n' + ' line\n', conf) + self.check('- >\n' + ' multi\n' + ' line\n', conf) + self.check('- key: >\n' + ' multi\n' + ' line\n', conf) + self.check('- key:\n' + ' >\n' + ' multi\n' + ' line\n', conf) + self.check('- ? >\n' + ' multi-line\n' + ' key\n' + ' : >\n' + ' multi-line\n' + ' value\n', conf) + self.check('- ?\n' + ' >\n' + ' multi-line\n' + ' key\n' + ' :\n' + ' >\n' + ' multi-line\n' + ' value\n', conf) + + def test_basics_literal_style(self): + conf = ('indentation: {spaces: 2}\n' + 'document-start: disable\n') + self.check('|\n' + ' multi\n' + ' line\n', conf) + self.check('- |\n' + ' multi\n' + ' line\n', conf) + self.check('- key: |\n' + ' multi\n' + ' line\n', conf) + self.check('- key:\n' + ' |\n' + ' multi\n' + ' line\n', conf) + self.check('- ? |\n' + ' multi-line\n' + ' key\n' + ' : |\n' + ' multi-line\n' + ' value\n', conf) + self.check('- ?\n' + ' |\n' + ' multi-line\n' + ' key\n' + ' :\n' + ' |\n' + ' multi-line\n' + ' value\n', conf) + + # The following "paragraph" examples are inspired from + # http://stackoverflow.com/questions/3790454/in-yaml-how-do-i-break-a-string-over-multiple-lines + + def test_paragraph_plain(self): + conf = ('indentation: {spaces: 2}\n' + 'document-start: disable\n') + self.check('- long text: very "long"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\n', conf) + self.check('- long text: very "long"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\n', conf, + problem1=(2, 5), problem2=(4, 5), problem3=(5, 5)) + self.check('- long text:\n' + ' very "long"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\n', conf) + + def test_paragraph_double_quoted(self): + conf = ('indentation: {spaces: 2}\n' + 'document-start: disable\n') + self.check('- long text: "very \\"long\\"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces."\n', conf) + self.check('- long text: "very \\"long\\"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces."\n', conf, + problem1=(2, 5), problem2=(4, 5), problem3=(5, 5)) + self.check('- long text: "very \\"long\\"\n' + '\'string\' with\n' + '\n' + 'paragraph gap, \\n and\n' + 'spaces."\n', conf, + problem1=(2, 1), problem2=(4, 1), problem3=(5, 1)) + self.check('- long text:\n' + ' "very \\"long\\"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces."\n', conf) + + def test_paragraph_single_quoted(self): + conf = ('indentation: {spaces: 2}\n' + 'document-start: disable\n') + self.check('- long text: \'very "long"\n' + ' \'\'string\'\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\'\n', conf) + self.check('- long text: \'very "long"\n' + ' \'\'string\'\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\'\n', conf, + problem1=(2, 5), problem2=(4, 5), problem3=(5, 5)) + self.check('- long text: \'very "long"\n' + '\'\'string\'\' with\n' + '\n' + 'paragraph gap, \\n and\n' + 'spaces.\'\n', conf, + problem1=(2, 1), problem2=(4, 1), problem3=(5, 1)) + self.check('- long text:\n' + ' \'very "long"\n' + ' \'\'string\'\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\'\n', conf) + + def test_paragraph_folded(self): + conf = ('indentation: {spaces: 2}\n' + 'document-start: disable\n') + self.check('- long text: >\n' + ' very "long"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\n', conf) + self.check('- long text: >\n' + ' very "long"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\n', conf, + problem1=(3, 6), problem2=(5, 7), problem3=(6, 8)) + + def test_paragraph_literal(self): + conf = ('indentation: {spaces: 2}\n' + 'document-start: disable\n') + self.check('- long text: |\n' + ' very "long"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\n', conf) + self.check('- long text: |\n' + ' very "long"\n' + ' \'string\' with\n' + '\n' + ' paragraph gap, \\n and\n' + ' spaces.\n', conf, + problem1=(3, 6), problem2=(5, 7), problem3=(6, 8)) diff --git a/yamllint/rules/indentation.py b/yamllint/rules/indentation.py index d1cec5b..12a64f3 100644 --- a/yamllint/rules/indentation.py +++ b/yamllint/rules/indentation.py @@ -35,6 +35,72 @@ class Parent(object): self.explicit_key = False +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 + # : > + # multi-line + # value + expected_indent = token.start_mark.column + conf['spaces'] + else: + # - key: > + # multi + # line + expected_indent = context['stack'][-2].indent + conf['spaces'] + else: + expected_indent = context['stack'][-1].indent + conf['spaces'] + + line_no = token.start_mark.line + 1 + + line_start = token.start_mark.pointer + while True: + line_start = token.start_mark.buffer.find( + '\n', line_start, token.end_mark.pointer - 1) + 1 + if line_start == 0: + break + line_no += 1 + + indent = 0 + while token.start_mark.buffer[line_start + indent] == ' ': + indent += 1 + if token.start_mark.buffer[line_start + indent] == '\n': + continue + + if indent != expected_indent: + yield LintProblem(line_no, indent + 1, + 'wrong indentation: expected %d but found %d' % + (expected_indent, indent)) + + def check(conf, token, prev, next, context): if 'stack' not in context: context['stack'] = [Parent(ROOT, 0)] @@ -42,11 +108,13 @@ def check(conf, token, prev, next, context): # Step 1: Lint - if (not isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)) and - not isinstance(token, yaml.BlockEndToken) and - not (isinstance(token, yaml.ScalarToken) and token.value == '') and - token.start_mark.line + 1 > context['cur_line']): + needs_lint = ( + not isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)) and + not isinstance(token, yaml.BlockEndToken) and + not (isinstance(token, yaml.ScalarToken) and token.value == '') and + token.start_mark.line + 1 > context['cur_line']) + if needs_lint: found_indentation = token.start_mark.column expected = context['stack'][-1].indent @@ -63,10 +131,17 @@ def check(conf, token, prev, next, context): 'wrong indentation: expected %d but found %d' % (expected, found_indentation)) + if isinstance(token, yaml.ScalarToken): + for problem in check_scalar_indentation(conf, token, context): + yield problem + + # Step 2.a: + + if needs_lint: context['cur_line_indent'] = found_indentation context['cur_line'] = token.end_mark.line + 1 - # Step 2: Update state + # Step 2.b: Update state if isinstance(token, yaml.BlockMappingStartToken): assert isinstance(next, yaml.KeyToken)