Compare commits

...

16 Commits

Author SHA1 Message Date
Adrien Vergé
33224a04e4 yamllint version 0.3.0 2016-01-19 22:58:59 +01:00
Adrien Vergé
fd9d2a00ff Doc: Update README with examples 2016-01-19 22:57:12 +01:00
Adrien Vergé
0b0251bacc Rules: indentation: Add the 'indent-sequences' option
Using either 'yes', 'no' or 'whatever', the user will be able to choose
whether to force block sequence items to be indented, to force them not
to be indented, or don't care, respectively.
2016-01-19 22:37:58 +01:00
Adrien Vergé
ad5cec9c6c Config: Allow overriding only one option when extending 2016-01-19 21:49:58 +01:00
Adrien Vergé
fb14cbdbd9 Config: Allow options to be in a pre-defined list 2016-01-19 21:12:11 +01:00
Adrien Vergé
8288a6f331 Rules: colons: Apply to '?' also 2016-01-19 19:45:13 +01:00
Adrien Vergé
9d8b0d4d2c Rules: commas: Don't allow a comma on a new line
Forbid such constructions:

    [ a, b, c
      , d, e ]
2016-01-19 19:42:56 +01:00
Adrien Vergé
39c878c819 Rules: indentation: Rewrite the algorithm (again)
Use a new, better thought algorithm that keeps an history stack with all
the parents indentations.
2016-01-19 19:42:56 +01:00
Adrien Vergé
222f7a27c1 Make syntax errors prevail over all yamllint problems 2016-01-19 17:18:57 +01:00
Adrien Vergé
effb4db3b4 Tests: Rules: Remove unused line and column args
Now that every test case use the `problem=(x, y)` syntax.
2016-01-19 17:18:57 +01:00
Adrien Vergé
d617eb70ae Rules: Keep a persistent context for token rules
This will be needed to build a clean indentation checking algorithm.
2016-01-19 17:18:57 +01:00
Adrien Vergé
f09aef4f89 Rules: comments-indentation: Allow two levels
Previously only comments that were indented like the following content
line were allowed, e.g.:

    prev: line:
      # commented line
      current: line

With this change, such new cases are also allowed:

      prev: line
      # commented line 1
    # commented line 2
    current: line
2016-01-19 17:18:57 +01:00
Adrien Vergé
01c12f2462 Syntax errors: Use the BaseLoader for safety 2016-01-19 16:35:57 +01:00
Adrien Vergé
918f15b68d Make syntax errors prevail over yamllint 'warnings'
When both a syntax error (unability to parse a document) and a cosmetic
yamllint problem are found at the same place, the yamllint problem had
the priority -- and the syntax error was not displayed.

This had the following problem: if a rule is at the 'warning' level, its
problems will not make the `yamllint` script return a failure return
code (`!= 0`), even when it should (because there was a syntax error,
precisely).

This commit changes this behavior by preferring yamllint problems only
when they have the 'error' level.
2016-01-15 18:46:49 +01:00
Adrien Vergé
97e2210ec9 Don't treat non-importable YAML as syntax error
`yaml.load()` exceptions are not necessarily syntax errors. For
instance, the following YAML source cannot be `load()`ed into a Python
object, but is valid nonetheless:

    ? - Detroit Tigers
      - Chicago cubs
    :
      - 2001-07-23

    ? [ New York Yankees,
        Atlanta Braves ]
    : [ 2001-07-02, 2001-08-12,
        2001-08-14 ]

This commit detects syntax errors from `yaml.parse()` exceptions rather
than `yaml.load_all()`.
2016-01-15 18:46:49 +01:00
Adrien Vergé
1934206cef Rules: comments-indentation: Fix typo 2016-01-15 18:46:40 +01:00
26 changed files with 811 additions and 129 deletions

View File

@@ -4,3 +4,63 @@ A linter for YAML files.
[![Build Status](https://travis-ci.org/adrienverge/yamllint.svg?branch=master)](https://travis-ci.org/adrienverge/yamllint)
[![Coverage Status](https://coveralls.io/repos/adrienverge/yamllint/badge.svg?branch=master&service=github)](https://coveralls.io/github/adrienverge/yamllint?branch=master)
Compatible with Python 2 & 3.
## Usage
```sh
yamllint my_file.yml my_other_file.yaml ...
```
```sh
yamllint -c ~/myconfig my_file.yml
```
```sh
# To output a format parsable (by editors like Vim, emacs, etc.)
yamllint -f parsable my_file.yml
```
## Installation
```sh
pip install yamllint
```
## Configuration
There is no documentation yet, so here is what you need to know: you can
override some rules, disable them or pass them in *warning* (instead of
*error*). Have a look at the content of `yamllint/conf/default.yml` and create
your own configuration file.
It could look like this:
```yaml
# This is my first, very own configuration file for yamllint!
# It extends the default conf by adjusting some options.
extends: default
rules:
# 80 should be enough, but don't fail if a line is longer
line-length:
max: 80
level: warning
# accept both
# key:
# - item
# and
# key:
# - item
indentation:
indent-sequences: whatever
# don't bother me with this rule
comments-indentation: disable
```
Tip: if you have a `.yamllint` file in your working directory, it will be
automatically loaded as configuration by yamllint.

View File

@@ -33,12 +33,19 @@ class RuleTestCase(unittest.TestCase):
'rules': conf}
return parse_config(yaml.safe_dump(conf))
def check(self, source, conf, line=None, column=None, **kwargs):
def check(self, source, conf, **kwargs):
expected_problems = []
for key in kwargs:
assert key.startswith('problem')
if len(kwargs[key]) > 2:
if kwargs[key][2] == 'syntax':
rule_id = None
else:
rule_id = kwargs[key][2]
else:
rule_id = self.rule_id
expected_problems.append(
LintProblem(kwargs[key][0], kwargs[key][1], rule=self.rule_id))
LintProblem(kwargs[key][0], kwargs[key][1], rule=rule_id))
expected_problems.sort()
real_problems = list(lint(source, self.build_fake_config(conf)))

View File

@@ -42,7 +42,7 @@ class ColonTestCase(RuleTestCase):
' - p: kdjf\n'
' - q: val0\n'
' - q2:\n'
' - val1\n'
' - val1\n'
'...\n', conf)
self.check('---\n'
'object:\n'
@@ -67,7 +67,7 @@ class ColonTestCase(RuleTestCase):
' - o: {k1: v1}\n'
' - o: {k1: v1}\n'
' - q2:\n'
' - val1\n'
' - val1\n'
'...\n', conf)
self.check('---\n'
'a: {b: {c: d, e : f}}\n', conf)
@@ -94,7 +94,7 @@ class ColonTestCase(RuleTestCase):
'...\n', conf, problem=(2, 4))
self.check('---\n'
'- lib :\n'
' - var\n'
' - var\n'
'...\n', conf, problem=(2, 6))
self.check('---\n'
'a: {b: {c : d, e : f}}\n', conf,
@@ -152,6 +152,21 @@ class ColonTestCase(RuleTestCase):
'a: {b: {c: d, e : f}}\n', conf,
problem1=(2, 12), problem2=(2, 20))
def test_after_enabled_question_mark(self):
conf = 'colons: {max-spaces-before: -1, max-spaces-after: 1}'
self.check('---\n'
'? key\n'
': value\n', conf)
self.check('---\n'
'? key\n'
': value\n', conf, problem=(2, 3))
self.check('---\n'
'? key\n'
': value\n', conf, problem1=(2, 3), problem2=(3, 3))
self.check('---\n'
'- ? key\n'
' : value\n', conf, problem1=(2, 5), problem2=(3, 5))
def test_after_max(self):
conf = 'colons: {max-spaces-before: -1, max-spaces-after: 3}'
self.check('---\n'

View File

@@ -152,3 +152,36 @@ class CommaTestCase(RuleTestCase):
problem1=(2, 12), problem2=(2, 16), problem3=(2, 31),
problem4=(2, 36), problem5=(2, 50), problem6=(4, 8),
problem7=(5, 11), problem8=(8, 13))
def test_comma_on_new_line(self):
conf = 'commas: {max-spaces-before: 0, max-spaces-after: 1}'
self.check('---\n'
'flow-seq: [1, 2, 3\n'
' , 4, 5, 6]\n'
'...\n', conf, problem=(3, 11))
self.check('---\n'
'flow-map: {a: 1, b: 2\n'
' , c: 3}\n'
'...\n', conf, problem=(3, 11))
conf = ('commas: {max-spaces-before: 0, max-spaces-after: 1}\n'
'indentation: disable\n')
self.check('---\n'
'flow-seq: [1, 2, 3\n'
' , 4, 5, 6]\n'
'...\n', conf, problem=(3, 9))
self.check('---\n'
'flow-map: {a: 1, b: 2\n'
' , c: 3}\n'
'...\n', conf, problem=(3, 9))
self.check('---\n'
'[\n'
'1,\n'
'2\n'
', 3\n'
']\n', conf, problem=(5, 1))
self.check('---\n'
'{\n'
'a: 1,\n'
'b: 2\n'
', c: 3\n'
'}\n', conf, problem=(5, 1))

View File

@@ -88,15 +88,39 @@ class CommentsIndentationTestCase(RuleTestCase):
self.check('---\n'
'obj1:\n'
' a: 1\n'
' # comments\n'
' # the following line is disabled\n'
' # b: 2\n', conf)
self.check('---\n'
'obj1:\n'
' a: 1\n'
' # b: 2\n'
'\n'
'obj2:\n'
' b: 2\n', conf, problem=(4, 3))
' b: 2\n', conf)
self.check('---\n'
'obj1:\n'
' a: 1\n'
' # b: 2\n'
'# this object is useless\n'
'obj2: no\n', conf)
self.check('---\n'
'obj1:\n'
' a: 1\n'
'# this object is useless\n'
' # b: 2\n'
'obj2: no\n', conf, problem=(5, 3))
self.check('---\n'
'obj1:\n'
' a: 1\n'
' # comments\n'
' b: 2\n', conf)
self.check('---\n'
'my list for today:\n'
' - todo 1\n'
' - todo 2\n'
' # commented for now\n'
' # - todo 3\n'
'...\n', conf)
def test_first_line(self):
conf = 'comments-indentation: {}'

View File

@@ -18,10 +18,32 @@ import unittest
import yaml
from yamllint.rules.common import Comment, get_comments_between_tokens
from yamllint.rules.common import (Comment, get_line_indent,
get_comments_between_tokens)
class CommonTestCase(unittest.TestCase):
def test_get_line_indent(self):
tokens = list(yaml.scan('a: 1\n'
'b:\n'
' - c: [2, 3, {d: 4}]\n'))
self.assertEqual(tokens[3].value, 'a')
self.assertEqual(tokens[5].value, '1')
self.assertEqual(tokens[7].value, 'b')
self.assertEqual(tokens[13].value, 'c')
self.assertEqual(tokens[16].value, '2')
self.assertEqual(tokens[18].value, '3')
self.assertEqual(tokens[22].value, 'd')
self.assertEqual(tokens[24].value, '4')
for i in (3, 5):
self.assertEqual(get_line_indent(tokens[i]), 0)
for i in (7,):
self.assertEqual(get_line_indent(tokens[i]), 0)
for i in (13, 16, 18, 22, 24):
self.assertEqual(get_line_indent(tokens[i]), 2)
def check_comments(self, buffer, *expected):
yaml_loader = yaml.BaseLoader(buffer)

View File

@@ -82,7 +82,7 @@ class DocumentStartTestCase(RuleTestCase):
'...\n'
'second: document\n'
'---\n'
'third: document\n', conf, problem=(4, 1))
'third: document\n', conf, problem=(4, 1, 'syntax'))
def test_directives(self):
conf = 'document-start: {present: yes}'

View File

@@ -36,12 +36,12 @@ class HyphenTestCase(RuleTestCase):
'- elem2\n', conf)
self.check('---\n'
'object:\n'
'- elem1\n'
'- elem2\n', conf)
' - elem1\n'
' - elem2\n', conf)
self.check('---\n'
'object:\n'
'- elem1\n'
'- elem2\n', conf)
' - elem1\n'
' - elem2\n', conf)
self.check('---\n'
'object:\n'
' subobject:\n'
@@ -69,12 +69,12 @@ class HyphenTestCase(RuleTestCase):
'- elem2\n', conf, problem=(2, 3))
self.check('---\n'
'object:\n'
'- elem1\n'
'- elem2\n', conf, problem=(4, 3))
' - elem1\n'
' - elem2\n', conf, problem=(4, 5))
self.check('---\n'
'object:\n'
'- elem1\n'
'- elem2\n', conf, problem1=(3, 3), problem2=(4, 3))
' - elem1\n'
' - elem2\n', conf, problem1=(3, 5), problem2=(4, 5))
self.check('---\n'
'object:\n'
' subobject:\n'

View File

@@ -48,37 +48,205 @@ class IndentationTestCase(RuleTestCase):
'...\n', conf)
def test_one_space(self):
conf = 'indentation: {spaces: 1}'
conf = 'indentation: {spaces: 1, indent-sequences: no}'
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)
conf = 'indentation: {spaces: 1, indent-sequences: yes}'
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)
def test_two_spaces(self):
conf = 'indentation: {spaces: 2}'
conf = 'indentation: {spaces: 2, indent-sequences: no}'
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)
conf = 'indentation: {spaces: 2, indent-sequences: yes}'
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)
def test_three_spaces(self):
conf = 'indentation: {spaces: 3}'
conf = 'indentation: {spaces: 3, indent-sequences: no}'
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)
conf = 'indentation: {spaces: 3, indent-sequences: yes}'
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)
def test_under_indented(self):
def test_indent_sequences_whatever(self):
conf = 'indentation: {spaces: 4, indent-sequences: whatever}'
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, problem=(3, 3))
self.check('---\n'
'list one:\n'
'- 1\n'
'- 2\n'
'- 3\n'
'list two:\n'
' - a\n'
' - b\n'
' - c\n', conf, problem=(7, 3))
self.check('---\n'
'list:\n'
' - 1\n'
' - 2\n'
' - 3\n'
'- a\n'
'- b\n'
'- c\n', conf, problem=(6, 1, 'syntax'))
def test_flow_mappings(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n'
'a: {x: 1,\n'
' y,\n'
' z: 1}\n', conf)
self.check('---\n'
'a: {x: 1,\n'
' y,\n'
' z: 1}\n', conf, problem=(3, 4))
self.check('---\n'
'a: {x: 1,\n'
' y,\n'
' z: 1}\n', conf, problem=(3, 6))
self.check('---\n'
'a: {x: 1,\n'
' y, z: 1\n'
'}\n', conf, problem=(3, 3))
self.check('---\n'
'a: {\n'
' x: 1,\n'
' y, z: 1\n'
'}\n', conf)
self.check('---\n'
'a: {\n'
' x: 1,\n'
' y, z: 1}\n', conf)
self.check('---\n'
'a: {\n'
' x: 1,\n'
' y, z: 1\n'
'}\n', conf, problem=(3, 4))
self.check('---\n'
'a: {\n'
' x: 1,\n'
' y, z: 1\n'
' }\n', conf, problem=(5, 3))
def test_flow_sequences(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n'
'a: [x,\n'
' y,\n'
' z]\n', conf)
self.check('---\n'
'a: [x,\n'
' y,\n'
' z]\n', conf, problem=(3, 4))
self.check('---\n'
'a: [x,\n'
' y,\n'
' z]\n', conf, problem=(3, 6))
self.check('---\n'
'a: [x,\n'
' y, z\n'
']\n', conf, problem=(3, 3))
self.check('---\n'
'a: [\n'
' x,\n'
' y, z\n'
']\n', conf)
self.check('---\n'
'a: [\n'
' x,\n'
' y, z]\n', conf)
self.check('---\n'
'a: [\n'
' x,\n'
' y, z\n'
']\n', conf, problem=(3, 4))
self.check('---\n'
'a: [\n'
' x,\n'
' y, z\n'
' ]\n', conf, problem=(5, 3))
def test_under_indented(self):
conf = 'indentation: {spaces: 2, indent-sequences: yes}'
self.check('---\n'
'object:\n'
' val: 1\n'
@@ -88,7 +256,13 @@ class IndentationTestCase(RuleTestCase):
' k1:\n'
' - a\n'
'...\n', conf, problem=(4, 4))
conf = 'indentation: {spaces: 4}'
self.check('---\n'
'object:\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
'...\n', conf, problem=(5, 6, 'syntax'))
conf = 'indentation: {spaces: 4, indent-sequences: yes}'
self.check('---\n'
'object:\n'
' val: 1\n'
@@ -98,9 +272,15 @@ class IndentationTestCase(RuleTestCase):
'- el2:\n'
' - subel\n'
'...\n', conf, problem=(4, 4))
self.check('---\n'
'object:\n'
' k3:\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf, problem=(5, 10, 'syntax'))
def test_over_indented(self):
conf = 'indentation: {spaces: 2}'
conf = 'indentation: {spaces: 2, indent-sequences: yes}'
self.check('---\n'
'object:\n'
' val: 1\n'
@@ -110,7 +290,13 @@ class IndentationTestCase(RuleTestCase):
' k1:\n'
' - a\n'
'...\n', conf, problem=(4, 6))
conf = 'indentation: {spaces: 4}'
self.check('---\n'
'object:\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
'...\n', conf, problem=(5, 12, 'syntax'))
conf = 'indentation: {spaces: 4, indent-sequences: yes}'
self.check('---\n'
'object:\n'
' val: 1\n'
@@ -118,7 +304,7 @@ class IndentationTestCase(RuleTestCase):
self.check('---\n'
' object:\n'
' val: 1\n'
'...\n', conf, problem1=(2, 2), problem2=(3, 6))
'...\n', conf, problem=(2, 2))
self.check('---\n'
'- el1\n'
'- el2:\n'
@@ -129,30 +315,57 @@ class IndentationTestCase(RuleTestCase):
'- el2:\n'
' - subel\n'
'...\n', conf, problem=(4, 15))
self.check('---\n'
' - el1\n'
' - el2:\n'
' - subel\n'
'...\n', conf,
problem=(2, 3))
self.check('---\n'
'object:\n'
' k3:\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf, problem=(5, 16, 'syntax'))
conf = 'indentation: {spaces: 4, indent-sequences: whatever}'
self.check('---\n'
' - el1\n'
' - el2:\n'
' - subel\n'
'...\n', conf,
problem1=(2, 3), problem2=(3, 3), problem3=(4, 5))
problem=(2, 3))
def test_multi_lines(self):
conf = 'indentation: {spaces: 2, indent-sequences: yes}'
self.check('---\n'
'long_string: >\n'
' bla bla blah\n'
' blah bla bla\n'
'...\n', None)
'...\n', conf)
self.check('---\n'
'- long_string: >\n'
' bla bla blah\n'
' blah bla bla\n'
'...\n', None)
'...\n', conf)
self.check('---\n'
'obj:\n'
' - long_string: >\n'
' bla bla blah\n'
' blah bla bla\n'
'...\n', None)
'...\n', conf)
def test_empty_value(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n'
'key1:\n'
'key2: not empty\n'
'key3:\n'
'...\n', conf)
self.check('---\n'
'-\n'
'- item 2\n'
'-\n'
'...\n', conf)
def test_nested_collections(self):
conf = 'indentation: {spaces: 2}'
@@ -163,7 +376,7 @@ class IndentationTestCase(RuleTestCase):
self.check('---\n'
'- o:\n'
' k1: v1\n'
'...\n', conf, problem=(3, 2))
'...\n', conf, problem=(3, 2, 'syntax'))
self.check('---\n'
'- o:\n'
' k1: v1\n'
@@ -171,18 +384,33 @@ class IndentationTestCase(RuleTestCase):
conf = 'indentation: {spaces: 4}'
self.check('---\n'
'- o:\n'
' k1: v1\n'
' k1: v1\n'
'...\n', conf)
self.check('---\n'
'- o:\n'
' k1: v1\n'
'...\n', conf, problem=(3, 4))
self.check('---\n'
'- o:\n'
' k1: v1\n'
'...\n', conf, problem=(3, 6))
self.check('---\n'
'- o:\n'
' k1: v1\n'
'...\n', conf, problem=(3, 8))
self.check('---\n'
'- - - - item\n'
' - elem 1\n'
' - elem 2\n'
' - - - - - very nested: a\n'
' key: value\n'
'...\n', conf)
self.check('---\n'
' - - - - item\n'
' - elem 1\n'
' - elem 2\n'
' - - - - - very nested: a\n'
' key: value\n'
'...\n', conf, problem=(2, 2))
def test_return(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n'
'a:\n'
' b:\n'
@@ -191,19 +419,19 @@ class IndentationTestCase(RuleTestCase):
' e:\n'
' f:\n'
'g:\n'
'...\n', None)
# self.check('---\n'
# 'a:\n'
# ' b:\n'
# ' c:\n'
# ' d:\n'
# '...\n', None, problem=(5, 5))
# self.check('---\n'
# 'a:\n'
# ' b:\n'
# ' c:\n'
# ' d:\n'
# '...\n', None, problem=(5, 2))
'...\n', conf)
self.check('---\n'
'a:\n'
' b:\n'
' c:\n'
' d:\n'
'...\n', conf, problem=(5, 4, 'syntax'))
self.check('---\n'
'a:\n'
' b:\n'
' c:\n'
' d:\n'
'...\n', conf, problem=(5, 2, 'syntax'))
def test_first_line(self):
conf = ('indentation: {spaces: 2}\n'

View File

@@ -35,3 +35,35 @@ class YamlLintTestCase(RuleTestCase):
'%TAG ! tag:clarkevans.com,2002:\n'
'doc: ument\n'
'...\n', None, problem=(3, 1))
def test_explicit_mapping(self):
self.check('---\n'
'? key\n'
': - value 1\n'
' - value 2\n'
'...\n', None)
self.check('---\n'
'?\n'
' key\n'
': {a: 1}\n'
'...\n', None)
self.check('---\n'
'?\n'
' key\n'
':\n'
' val\n'
'...\n', None)
def test_mapping_between_sequences(self):
# This is valid YAML. See http://www.yaml.org/spec/1.2/spec.html,
# example 2.11
self.check('---\n'
'? - Detroit Tigers\n'
' - Chicago cubs\n'
':\n'
' - 2001-07-23\n'
'\n'
'? [New York Yankees,\n'
' Atlanta Braves]\n'
': [2001-07-02, 2001-08-12,\n'
' 2001-08-14]\n', None)

View File

@@ -33,11 +33,11 @@ class TrailingSpacesTestCase(RuleTestCase):
self.check('', conf)
self.check('\n', conf)
self.check(' \n', conf, problem=(1, 1))
self.check('\t\t\t\n', conf, problem=(1, 1))
self.check('\t\t\t\n', conf, problem=(1, 1, 'syntax'))
self.check('---\n'
'some: text \n', conf, problem=(2, 11))
self.check('---\n'
'some: text\t\n', conf, problem=(2, 11))
'some: text\t\n', conf, problem=(2, 11, 'syntax'))
def test_with_dos_new_lines(self):
conf = ('trailing-spaces: {}\n'

69
tests/test_config.py Normal file
View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import unittest
from yamllint import config
class ConfigTestCase(unittest.TestCase):
def setUp(self):
self.base = config.parse_config_from_file(os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
'yamllint', 'conf', 'default.yml'))
def test_extend_config_disable_rule(self):
new = config.parse_config('extends: default\n'
'rules:\n'
' trailing-spaces: disable\n')
base = self.base.copy()
del base['trailing-spaces']
self.assertEqual(sorted(new.keys()), sorted(base.keys()))
for rule in new:
self.assertEqual(new[rule], base[rule])
def test_extend_config_override_whole_rule(self):
new = config.parse_config('extends: default\n'
'rules:\n'
' empty-lines:\n'
' max: 42\n'
' max-start: 43\n'
' max-end: 44\n')
base = self.base.copy()
base['empty-lines']['max'] = 42
base['empty-lines']['max-start'] = 43
base['empty-lines']['max-end'] = 44
self.assertEqual(sorted(new.keys()), sorted(base.keys()))
for rule in new:
self.assertEqual(new[rule], base[rule])
def test_extend_config_override_rule_partly(self):
new = config.parse_config('extends: default\n'
'rules:\n'
' empty-lines:\n'
' max-start: 42\n')
base = self.base.copy()
base['empty-lines']['max-start'] = 42
self.assertEqual(sorted(new.keys()), sorted(base.keys()))
for rule in new:
self.assertEqual(new[rule], base[rule])

View File

@@ -22,7 +22,7 @@ from yamllint import parser
APP_NAME = 'yamllint'
APP_VERSION = '0.2.0'
APP_VERSION = '0.3.0'
APP_DESCRIPTION = 'A linter for YAML files.'
__author__ = 'Adrien Vergé'
@@ -38,12 +38,17 @@ def get_costemic_problems(buffer, conf):
token_rules = [r for r in rules if r.TYPE == 'token']
line_rules = [r for r in rules if r.TYPE == 'line']
context = {}
for rule in token_rules:
context[rule.ID] = {}
for elem in parser.token_or_line_generator(buffer):
if isinstance(elem, parser.Token):
for rule in token_rules:
rule_conf = conf[rule.ID]
for problem in rule.check(rule_conf,
elem.curr, elem.prev, elem.next):
elem.curr, elem.prev, elem.next,
context[rule.ID]):
problem.rule = rule.ID
problem.level = rule_conf['level']
yield problem
@@ -58,7 +63,7 @@ def get_costemic_problems(buffer, conf):
def get_syntax_error(buffer):
try:
list(yaml.safe_load_all(buffer))
list(yaml.parse(buffer, Loader=yaml.BaseLoader))
except yaml.error.MarkedYAMLError as e:
problem = LintProblem(e.problem_mark.line + 1,
e.problem_mark.column + 1,
@@ -76,11 +81,16 @@ def _lint(buffer, conf):
# Insert the syntax error (if any) at the right place...
if (syntax_error and syntax_error.line <= problem.line and
syntax_error.column <= problem.column):
# ... unless there is already a yamllint error, in which case the
# syntax error is probably redundant.
if (syntax_error.line != problem.line or
syntax_error.column != problem.column):
yield syntax_error
yield syntax_error
# If there is already a yamllint error at the same place, discard
# it as it is probably redundant (and maybe it's just a 'warning',
# in which case the script won't even exit with a failure status).
if (syntax_error.line == problem.line and
syntax_error.column == problem.column):
syntax_error = None
continue
syntax_error = None
yield problem

View File

@@ -31,12 +31,10 @@ rules:
max-spaces-after: 1
indentation:
spaces: 2
indent-sequences: yes
line-length:
max: 80
new-line-at-end-of-file: {level: error}
new-lines:
type: unix
#sequences-indentation:
# level: warning
# present: yes
trailing-spaces: {}

View File

@@ -46,7 +46,11 @@ def extend_config(content):
if 'extends' in conf:
base = parse_config_from_file(get_extended_conf(conf['extends']))
base.update(conf['rules'])
for rule in conf['rules']:
if type(conf['rules'][rule]) == dict and rule in base:
base[rule].update(conf['rules'][rule])
else:
base[rule] = conf['rules'][rule]
conf['rules'] = base
return conf
@@ -83,10 +87,16 @@ def parse_config(content):
raise YamlLintConfigError(
'invalid config: unknown option "%s" for rule "%s"' %
(optkey, id))
if type(conf['rules'][id][optkey]) != options[optkey]:
raise YamlLintConfigError(
'invalid config: option "%s" of "%s" should be %s' %
(optkey, id, options[optkey].__name__))
if type(options[optkey]) == tuple:
if conf['rules'][id][optkey] not in options[optkey]:
raise YamlLintConfigError(
('invalid config: option "%s" of "%s" should be '
'in %s') % (optkey, id, options[optkey]))
else:
if type(conf['rules'][id][optkey]) != options[optkey]:
raise YamlLintConfigError(
('invalid config: option "%s" of "%s" should be '
'%s' % (optkey, id, options[optkey].__name__)))
rules[id][optkey] = conf['rules'][id][optkey]
else:
raise YamlLintConfigError(('invalid config: rule "%s": should be '

View File

@@ -25,7 +25,7 @@ CONF = {'min-spaces-inside': int,
'max-spaces-inside': int}
def check(conf, token, prev, next):
def check(conf, token, prev, next, context):
if isinstance(token, yaml.FlowMappingStartToken):
problem = spaces_after(token, prev, next,
min=conf['min-spaces-inside'],

View File

@@ -25,7 +25,7 @@ CONF = {'min-spaces-inside': int,
'max-spaces-inside': int}
def check(conf, token, prev, next):
def check(conf, token, prev, next, context):
if isinstance(token, yaml.FlowSequenceStartToken):
problem = spaces_after(token, prev, next,
min=conf['min-spaces-inside'],

View File

@@ -16,7 +16,7 @@
import yaml
from yamllint.rules.common import spaces_after, spaces_before
from yamllint.rules.common import spaces_after, spaces_before, is_explicit_key
ID = 'colons'
@@ -25,7 +25,7 @@ CONF = {'max-spaces-before': int,
'max-spaces-after': int}
def check(conf, token, prev, next):
def check(conf, token, prev, next, context):
if isinstance(token, yaml.ValueToken):
problem = spaces_before(token, prev, next,
max=conf['max-spaces-before'],
@@ -38,3 +38,10 @@ def check(conf, token, prev, next):
max_desc='too many spaces after colon')
if problem is not None:
yield problem
if isinstance(token, yaml.KeyToken) and is_explicit_key(token):
problem = spaces_after(token, prev, next,
max=conf['max-spaces-after'],
max_desc='too many spaces after question mark')
if problem is not None:
yield problem

View File

@@ -16,6 +16,7 @@
import yaml
from yamllint.errors import LintProblem
from yamllint.rules.common import spaces_after, spaces_before
@@ -25,13 +26,18 @@ CONF = {'max-spaces-before': int,
'max-spaces-after': int}
def check(conf, token, prev, next):
def check(conf, token, prev, next, context):
if isinstance(token, yaml.FlowEntryToken):
problem = spaces_before(token, prev, next,
max=conf['max-spaces-before'],
max_desc='too many spaces before comma')
if problem is not None:
yield problem
if prev is not None and prev.end_mark.line < token.start_mark.line:
yield LintProblem(token.start_mark.line + 1,
max(1, token.start_mark.column),
'too many spaces before comma')
else:
problem = spaces_before(token, prev, next,
max=conf['max-spaces-before'],
max_desc='too many spaces before comma')
if problem is not None:
yield problem
problem = spaces_after(token, prev, next,
max=conf['max-spaces-after'],

View File

@@ -26,7 +26,7 @@ CONF = {'require-starting-space': bool,
'min-spaces-from-content': int}
def check(conf, token, prev, next):
def check(conf, token, prev, next, context):
for comment in get_comments_between_tokens(token, next):
if (conf['min-spaces-from-content'] != -1 and
not isinstance(token, yaml.StreamStartToken) and

View File

@@ -17,27 +17,48 @@
import yaml
from yamllint.errors import LintProblem
from yamllint.rules.common import get_comments_between_tokens
from yamllint.rules.common import get_line_indent, get_comments_between_tokens
ID = 'comments-indentation'
TYPE = 'token'
def check(conf, token, prev, next):
# Case A:
#
# prev: line:
# # commented line
# current: line
#
# Case B:
#
# prev: line
# # commented line 1
# # commented line 2
# current: line
def check(conf, token, prev, next, context):
if prev is None:
return
token_indent = token.start_mark.column
curr_line_indent = token.start_mark.column
if isinstance(token, yaml.StreamEndToken):
token_indent = 0
curr_line_indent = 0
skip_first = True
skip_first_line = True
if isinstance(prev, yaml.StreamStartToken):
skip_first = False
skip_first_line = False
prev_line_indent = 0
else:
prev_line_indent = get_line_indent(prev)
for comment in get_comments_between_tokens(prev, token,
skip_first_line=skip_first):
if comment.column != token_indent + 1:
if prev_line_indent <= curr_line_indent:
prev_line_indent = -1 # disable it
for comment in get_comments_between_tokens(
prev, token, skip_first_line=skip_first_line):
if comment.column - 1 == curr_line_indent:
prev_line_indent = -1 # disable it
elif comment.column - 1 != prev_line_indent:
yield LintProblem(comment.line, comment.column,
'comment not intended like content')
'comment not indented like content')

View File

@@ -64,6 +64,16 @@ class Comment(object):
str(self) == str(other))
def get_line_indent(token):
"""Finds the indent of the line the token starts in."""
start = token.start_mark.buffer.rfind('\n', 0,
token.start_mark.pointer) + 1
content = start
while token.start_mark.buffer[content] == ' ':
content += 1
return content - start
def get_comments_between_tokens(token1, token2, skip_first_line=False):
if token2 is None:
buf = token1.end_mark.buffer[token1.end_mark.pointer:]
@@ -91,3 +101,15 @@ def get_comments_between_tokens(token1, token2, skip_first_line=False):
pointer += len(line) + 1
line_no += 1
column_no = 1
def is_explicit_key(token):
# explicit key:
# ? key
# : v
# or
# ?
# key
# : v
return (token.start_mark.pointer < token.end_mark.pointer and
token.start_mark.buffer[token.start_mark.pointer] == '?')

View File

@@ -24,7 +24,7 @@ TYPE = 'token'
CONF = {'present': bool}
def check(conf, token, prev, next):
def check(conf, token, prev, next, context):
if conf['present']:
if (isinstance(token, yaml.StreamEndToken) and
not (isinstance(prev, yaml.DocumentEndToken) or

View File

@@ -24,7 +24,7 @@ TYPE = 'token'
CONF = {'present': bool}
def check(conf, token, prev, next):
def check(conf, token, prev, next, context):
if conf['present']:
if (isinstance(prev, (yaml.StreamStartToken,
yaml.DocumentEndToken,

View File

@@ -24,7 +24,7 @@ TYPE = 'token'
CONF = {'max-spaces-after': int}
def check(conf, token, prev, next):
def check(conf, token, prev, next, context):
if isinstance(token, yaml.BlockEntryToken):
problem = spaces_after(token, prev, next,
max=conf['max-spaces-after'],

View File

@@ -17,54 +17,172 @@
import yaml
from yamllint.errors import LintProblem
from yamllint.rules.common import is_explicit_key
ID = 'indentation'
TYPE = 'token'
CONF = {'spaces': int}
CONF = {'spaces': int,
'indent-sequences': (True, False, 'whatever')}
ROOT, MAP, B_SEQ, F_SEQ, KEY, VAL = range(6)
def check(conf, token, prev, next):
if isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)):
return
class Parent(object):
def __init__(self, type, indent):
self.type = type
self.indent = indent
self.explicit_key = False
# Check if first token in line
if (not isinstance(prev, (yaml.StreamStartToken, yaml.DirectiveToken)) and
token.start_mark.line == prev.end_mark.line):
return
if token.start_mark.column % conf['spaces'] != 0:
yield LintProblem(
token.end_mark.line + 1, token.start_mark.column + 1,
'indentation is not a multiple of %d' % conf['spaces'])
return
def check(conf, token, prev, next, context):
if 'stack' not in context:
context['stack'] = [Parent(ROOT, 0)]
context['cur_line'] = -1
if isinstance(prev, (yaml.StreamStartToken,
yaml.DirectiveToken,
yaml.DocumentStartToken,
yaml.DocumentEndToken)):
indent = 0
else:
buffer = prev.end_mark.buffer
start = buffer.rfind('\n', 0, prev.end_mark.pointer) + 1
# Step 1: Lint
indent = 0
while buffer[start + indent] == ' ':
indent += 1
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']):
if token.start_mark.column > indent:
if not isinstance(prev, (yaml.BlockSequenceStartToken,
yaml.BlockMappingStartToken,
yaml.FlowSequenceStartToken,
yaml.FlowMappingStartToken,
yaml.KeyToken,
yaml.ValueToken)):
yield LintProblem(
token.end_mark.line + 1, token.start_mark.column + 1,
'unexpected indentation')
found_indentation = token.start_mark.column
expected = context['stack'][-1].indent
elif token.start_mark.column != indent + conf['spaces']:
yield LintProblem(
token.end_mark.line + 1, token.start_mark.column + 1,
'found indentation of %d instead of %d' %
(token.start_mark.column, indent + conf['spaces']))
if isinstance(token, (yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken)):
expected = 0
elif (context['stack'][-1].type == KEY and
context['stack'][-1].explicit_key and
not isinstance(token, yaml.ValueToken)):
expected += conf['spaces']
if found_indentation != expected:
yield LintProblem(token.start_mark.line + 1, found_indentation + 1,
'wrong indentation: expected %d but found %d' %
(expected, found_indentation))
context['cur_line_indent'] = found_indentation
context['cur_line'] = token.end_mark.line + 1
# Step 2: Update state
if isinstance(token, yaml.BlockMappingStartToken):
assert isinstance(next, yaml.KeyToken)
if next.start_mark.line == token.start_mark.line:
# - a: 1
# b: 2
# or
# - ? a
# : 1
indent = token.start_mark.column
else:
# - ?
# a
# : 1
indent = token.start_mark.column + conf['spaces']
context['stack'].append(Parent(MAP, indent))
elif isinstance(token, yaml.FlowMappingStartToken):
if next.start_mark.line == token.start_mark.line:
# - {a: 1, b: 2}
indent = next.start_mark.column
else:
# - {
# a: 1, b: 2
# }
indent = context['cur_line_indent'] + conf['spaces']
context['stack'].append(Parent(MAP, indent))
elif isinstance(token, yaml.BlockSequenceStartToken):
# - - a
# - b
assert next.start_mark.line == token.start_mark.line
assert isinstance(next, yaml.BlockEntryToken)
indent = token.start_mark.column
context['stack'].append(Parent(B_SEQ, indent))
elif isinstance(token, yaml.FlowSequenceStartToken):
if next.start_mark.line == token.start_mark.line:
# - [a, b]
indent = next.start_mark.column
else:
# - [
# a, b
# ]
indent = context['cur_line_indent'] + conf['spaces']
context['stack'].append(Parent(F_SEQ, indent))
elif isinstance(token, (yaml.BlockEndToken,
yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken)):
assert context['stack'][-1].type in (MAP, B_SEQ, F_SEQ)
context['stack'].pop()
elif isinstance(token, yaml.KeyToken):
indent = context['stack'][-1].indent
context['stack'].append(Parent(KEY, indent))
context['stack'][-1].explicit_key = is_explicit_key(token)
if context['stack'][-1].type == VAL:
context['stack'].pop()
assert context['stack'][-1].type == KEY
context['stack'].pop()
elif isinstance(token, yaml.ValueToken):
assert context['stack'][-1].type == KEY
# Discard empty values
if isinstance(next, (yaml.BlockEndToken,
yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken,
yaml.KeyToken)):
context['stack'].pop()
else:
if context['stack'][-1].explicit_key:
# ? k
# : value
# or
# ? k
# :
# value
indent = context['stack'][-1].indent + conf['spaces']
elif next.start_mark.line == prev.start_mark.line:
# k: value
indent = next.start_mark.column
elif isinstance(next, (yaml.BlockSequenceStartToken,
yaml.BlockEntryToken)):
# NOTE: We add BlockEntryToken in the test above because
# sometimes BlockSequenceStartToken are not issued. Try
# yaml.scan()ning this:
# '- lib:\n'
# ' - var\n'
if conf['indent-sequences'] is False:
indent = context['stack'][-1].indent
elif conf['indent-sequences'] is True:
indent = context['stack'][-1].indent + conf['spaces']
else: # 'whatever'
if next.start_mark.column == context['stack'][-1].indent:
# key:
# - e1
# - e2
indent = context['stack'][-1].indent
else:
# key:
# - e1
# - e2
indent = context['stack'][-1].indent + conf['spaces']
else:
# k:
# value
indent = context['stack'][-1].indent + conf['spaces']
context['stack'].append(Parent(VAL, indent))