Compare commits

...

24 Commits

Author SHA1 Message Date
Adrien Vergé
7b6f024448 yamllint version 0.4.0 2016-01-20 18:18:35 +01:00
Adrien Vergé
75b4758c95 cli: 'standard' format: Print filename only when error 2016-01-20 17:55:54 +01:00
Adrien Vergé
0e98df2643 cli: Allow passing directories as arguments
For instance:

    yamllint .
    yamllint file.yml ../my-other-dir
2016-01-20 17:55:54 +01:00
Adrien Vergé
d4189083d0 Introduce the 'cli' module and call it from the script 2016-01-20 17:39:26 +01:00
Adrien Vergé
67d13d60ae Rules: indentation: Check multi-line scalars 2016-01-20 17:39:11 +01:00
Adrien Vergé
96465008ab Rules: Fix spaces_before when prev is multi-line scalar
YAML content like the following one produced an error, because the
multi-line ScalarToken ends at the beginning of the 4th line (the one
with the value):

    ? >
        multi-line
        key
    : value
2016-01-20 17:38:48 +01:00
Adrien Vergé
847f7e3fff Rules: comments: Fix bug when multi-line scalar
YAML content like the following one produced an error, because the
ScalarToken associated whose value is "this is plain text" ends at the
beginning of the 5th line (the one with the comment):

    ---
    string: >
      this is plain text

    # comment
2016-01-20 10:45:59 +01:00
Adrien Vergé
6a24781f96 Tests: indentation: Add explicit keys test cases 2016-01-20 10:45:52 +01:00
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
30 changed files with 1327 additions and 231 deletions

View File

@@ -4,3 +4,67 @@ 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 .
```
```sh
yamllint -c ~/myconfig file.yml
```
```sh
# To output a format parsable (by editors like Vim, emacs, etc.)
yamllint -f parsable 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

@@ -15,68 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import os.path
import sys
import argparse
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
from yamllint import config
from yamllint.errors import YamlLintConfigError
from yamllint import lint
from yamllint import output
from yamllint import cli
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog=APP_NAME,
description=APP_DESCRIPTION)
parser.add_argument('files', metavar='FILES', nargs='+',
help='files to check')
parser.add_argument('-c', '--config', dest='config_file', action='store',
help='path to a custom configuration')
parser.add_argument('-f', '--format',
choices=('parsable', 'standard'), default='standard',
help='format for parsing output')
parser.add_argument('-v', '--version', action='version',
version='%s %s' % (APP_NAME, APP_VERSION))
# TODO: read from stdin when no filename?
args = parser.parse_args()
try:
if args.config_file is not None:
conf = config.parse_config_from_file(args.config_file)
elif os.path.isfile('.yamllint'):
conf = config.parse_config_from_file('.yamllint')
else:
conf = config.parse_config('extends: default')
except YamlLintConfigError as e:
print(e, file=sys.stderr)
sys.exit(-1)
return_code = 0
for file in args.files:
if args.format != 'parsable':
print('\033[4m%s\033[0m' % file)
try:
with open(file) as f:
for problem in lint(f, conf):
if args.format == 'parsable':
print(output.parsable_format(problem, file))
else:
print(output.standard_format(problem, file))
if return_code == 0 and problem.level == 'error':
return_code = 1
except EnvironmentError as e:
print(e)
return_code = -1
if args.format != 'parsable':
print('')
sys.exit(return_code)
cli.run(sys.argv[1:])

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,
@@ -118,7 +118,7 @@ class ColonTestCase(RuleTestCase):
'...\n', conf, problem=(3, 8))
def test_before_with_explicit_block_mappings(self):
conf = 'colons: {max-spaces-before: 0, max-spaces-after: -1}'
conf = 'colons: {max-spaces-before: 0, max-spaces-after: 1}'
self.check('---\n'
'object:\n'
' ? key\n'
@@ -129,6 +129,30 @@ class ColonTestCase(RuleTestCase):
' ? key\n'
' : value\n'
'...\n', conf, problem=(2, 7))
self.check('---\n'
'? >\n'
' multi-line\n'
' key\n'
': >\n'
' multi-line\n'
' value\n'
'...\n', conf)
self.check('---\n'
'- ? >\n'
' multi-line\n'
' key\n'
' : >\n'
' multi-line\n'
' value\n'
'...\n', conf)
self.check('---\n'
'- ? >\n'
' multi-line\n'
' key\n'
' : >\n'
' multi-line\n'
' value\n'
'...\n', conf, problem=(5, 5))
def test_after_enabled(self):
conf = 'colons: {max-spaces-before: -1, max-spaces-after: 1}'
@@ -152,6 +176,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

@@ -131,3 +131,19 @@ class CommentsTestCase(RuleTestCase):
' require-starting-space: yes\n'
' min-spaces-from-content: 2\n')
self.check('# comment\n', conf)
def test_multi_line_scalar(self):
conf = ('comments:\n'
' require-starting-space: yes\n'
' min-spaces-from-content: 2\n'
'trailing-spaces: disable\n')
self.check('---\n'
'string: >\n'
' this is plain text\n'
'\n'
'# comment\n', conf)
self.check('---\n'
'- string: >\n'
' this is plain text\n'
' \n'
' # comment\n', conf)

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'
@@ -224,3 +452,270 @@ class IndentationTestCase(RuleTestCase):
' 2,\n'
' 3\n'
']\n', conf, problem1=(4, 4), problem2=(5, 2))
def test_explicit_block_mappings(self):
conf = 'indentation: {spaces: 4}'
self.check('---\n'
'object:\n'
' ? key\n'
' :\n'
' value\n'
'...\n', conf)
self.check('---\n'
'object:\n'
' ? key\n'
' :\n'
' value\n'
'...\n', conf, problem=(5, 8))
self.check('---\n'
'object:\n'
' ?\n'
' key\n'
' :\n'
' value\n'
'...\n', conf)
self.check('---\n'
'object:\n'
' ?\n'
' key\n'
' :\n'
' value\n'
'...\n', conf, problem1=(4, 8), problem2=(6, 10))
self.check('---\n'
'object:\n'
' ?\n'
' key\n'
' :\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))

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.4.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

119
yamllint/cli.py Normal file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python
# -*- 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/>.
from __future__ import print_function
import os.path
import sys
import argparse
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
from yamllint import config
from yamllint.errors import YamlLintConfigError
from yamllint import lint
def find_files_recursively(items):
for item in items:
if os.path.isdir(item):
for root, dirnames, filenames in os.walk(item):
for filename in [f for f in filenames
if f.endswith(('.yml', '.yaml'))]:
yield os.path.join(root, filename)
else:
yield item
class Format(object):
@staticmethod
def parsable(problem, filename):
return ('%(file)s:%(line)s:%(column)s: [%(level)s] %(message)s' %
{'file': filename,
'line': problem.line,
'column': problem.column,
'level': problem.level,
'message': problem.message})
@staticmethod
def standard(problem, filename):
line = ' \033[2m%d:%d\033[0m' % (problem.line, problem.column)
line += max(20 - len(line), 0) * ' '
if problem.level == 'warning':
line += '\033[33m%s\033[0m' % problem.level
else:
line += '\033[31m%s\033[0m' % problem.level
line += max(38 - len(line), 0) * ' '
line += problem.desc
if problem.rule:
line += ' \033[2m(%s)\033[0m' % problem.rule
return line
def run(argv):
parser = argparse.ArgumentParser(prog=APP_NAME,
description=APP_DESCRIPTION)
parser.add_argument('files', metavar='FILE_OR_DIR', nargs='+',
help='files to check')
parser.add_argument('-c', '--config', dest='config_file', action='store',
help='path to a custom configuration')
parser.add_argument('-f', '--format',
choices=('parsable', 'standard'), default='standard',
help='format for parsing output')
parser.add_argument('-v', '--version', action='version',
version='%s %s' % (APP_NAME, APP_VERSION))
# TODO: read from stdin when no filename?
args = parser.parse_args(argv)
try:
if args.config_file is not None:
conf = config.parse_config_from_file(args.config_file)
elif os.path.isfile('.yamllint'):
conf = config.parse_config_from_file('.yamllint')
else:
conf = config.parse_config('extends: default')
except YamlLintConfigError as e:
print(e, file=sys.stderr)
sys.exit(-1)
return_code = 0
for file in find_files_recursively(args.files):
try:
first = True
with open(file) as f:
for problem in lint(f, conf):
if args.format == 'parsable':
print(Format.parsable(problem, file))
else:
if first:
print('\033[4m%s\033[0m' % file)
first = False
print(Format.standard(problem, file))
if return_code == 0 and problem.level == 'error':
return_code = 1
if not first and args.format != 'parsable':
print('')
except EnvironmentError as e:
print(e)
return_code = -1
sys.exit(return_code)

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

@@ -1,38 +0,0 @@
# -*- 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/>.
def parsable_format(problem, filename):
return ('%(file)s:%(line)s:%(column)s: [%(level)s] %(message)s' %
{'file': filename,
'line': problem.line,
'column': problem.column,
'level': problem.level,
'message': problem.message})
def standard_format(problem, filename):
line = ' \033[2m%d:%d\033[0m' % (problem.line, problem.column)
line += max(20 - len(line), 0) * ' '
if problem.level == 'warning':
line += '\033[33m%s\033[0m' % problem.level
else:
line += '\033[31m%s\033[0m' % problem.level
line += max(38 - len(line), 0) * ' '
line += problem.desc
if problem.rule:
line += ' \033[2m(%s)\033[0m' % problem.rule
return line

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,15 +26,17 @@ 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
comment.line == token.end_mark.line + 1 and
comment.pointer - token.end_mark.pointer <
conf['min-spaces-from-content']):
yield LintProblem(comment.line, comment.column,
'too few spaces before comment')
comment.line == token.end_mark.line + 1):
# Sometimes token end marks are on the next line
if token.end_mark.buffer[token.end_mark.pointer - 1] != '\n':
if (comment.pointer - token.end_mark.pointer <
conf['min-spaces-from-content']):
yield LintProblem(comment.line, comment.column,
'too few spaces before comment')
if (conf['require-starting-space'] and
comment.pointer + 1 < len(comment.buffer) 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

@@ -33,7 +33,10 @@ def spaces_after(token, prev, next, min=-1, max=-1,
def spaces_before(token, prev, next, min=-1, max=-1,
min_desc=None, max_desc=None):
if prev is not None and prev.end_mark.line == token.start_mark.line:
if (prev is not None and prev.end_mark.line == token.start_mark.line and
# Discard tokens (only scalars?) that end at the start of next line
(prev.end_mark.pointer == 0 or
prev.end_mark.buffer[prev.end_mark.pointer - 1] != '\n')):
spaces = token.start_mark.pointer - prev.end_mark.pointer
if max != - 1 and spaces > max:
return LintProblem(token.start_mark.line + 1,
@@ -64,6 +67,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 +104,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,247 @@
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)):
class Parent(object):
def __init__(self, type, indent):
self.type = type
self.indent = indent
self.explicit_key = False
def check_scalar_indentation(conf, token, context):
if token.start_mark.line == token.end_mark.line:
return
# 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.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']
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
line_no = token.start_mark.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
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 buffer[start + indent] == ' ':
while token.start_mark.buffer[line_start + indent] == ' ':
indent += 1
if token.start_mark.buffer[line_start + indent] == '\n':
continue
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')
if indent != expected_indent:
yield LintProblem(line_no, indent + 1,
'wrong indentation: expected %d but found %d' %
(expected_indent, 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']))
def check(conf, token, prev, next, context):
if 'stack' not in context:
context['stack'] = [Parent(ROOT, 0)]
context['cur_line'] = -1
# Step 1: Lint
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
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))
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.b: 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))