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) [![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) [![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 # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import os.path
import sys import sys
import argparse from yamllint import cli
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
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(prog=APP_NAME, cli.run(sys.argv[1:])
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)

View File

@@ -33,12 +33,19 @@ class RuleTestCase(unittest.TestCase):
'rules': conf} 'rules': conf}
return parse_config(yaml.safe_dump(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 = [] expected_problems = []
for key in kwargs: for key in kwargs:
assert key.startswith('problem') 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( 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() expected_problems.sort()
real_problems = list(lint(source, self.build_fake_config(conf))) real_problems = list(lint(source, self.build_fake_config(conf)))

View File

@@ -42,7 +42,7 @@ class ColonTestCase(RuleTestCase):
' - p: kdjf\n' ' - p: kdjf\n'
' - q: val0\n' ' - q: val0\n'
' - q2:\n' ' - q2:\n'
' - val1\n' ' - val1\n'
'...\n', conf) '...\n', conf)
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
@@ -67,7 +67,7 @@ class ColonTestCase(RuleTestCase):
' - o: {k1: v1}\n' ' - o: {k1: v1}\n'
' - o: {k1: v1}\n' ' - o: {k1: v1}\n'
' - q2:\n' ' - q2:\n'
' - val1\n' ' - val1\n'
'...\n', conf) '...\n', conf)
self.check('---\n' self.check('---\n'
'a: {b: {c: d, e : f}}\n', conf) 'a: {b: {c: d, e : f}}\n', conf)
@@ -94,7 +94,7 @@ class ColonTestCase(RuleTestCase):
'...\n', conf, problem=(2, 4)) '...\n', conf, problem=(2, 4))
self.check('---\n' self.check('---\n'
'- lib :\n' '- lib :\n'
' - var\n' ' - var\n'
'...\n', conf, problem=(2, 6)) '...\n', conf, problem=(2, 6))
self.check('---\n' self.check('---\n'
'a: {b: {c : d, e : f}}\n', conf, 'a: {b: {c : d, e : f}}\n', conf,
@@ -118,7 +118,7 @@ class ColonTestCase(RuleTestCase):
'...\n', conf, problem=(3, 8)) '...\n', conf, problem=(3, 8))
def test_before_with_explicit_block_mappings(self): 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' self.check('---\n'
'object:\n' 'object:\n'
' ? key\n' ' ? key\n'
@@ -129,6 +129,30 @@ class ColonTestCase(RuleTestCase):
' ? key\n' ' ? key\n'
' : value\n' ' : value\n'
'...\n', conf, problem=(2, 7)) '...\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): def test_after_enabled(self):
conf = 'colons: {max-spaces-before: -1, max-spaces-after: 1}' 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, 'a: {b: {c: d, e : f}}\n', conf,
problem1=(2, 12), problem2=(2, 20)) 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): def test_after_max(self):
conf = 'colons: {max-spaces-before: -1, max-spaces-after: 3}' conf = 'colons: {max-spaces-before: -1, max-spaces-after: 3}'
self.check('---\n' self.check('---\n'

View File

@@ -152,3 +152,36 @@ class CommaTestCase(RuleTestCase):
problem1=(2, 12), problem2=(2, 16), problem3=(2, 31), problem1=(2, 12), problem2=(2, 16), problem3=(2, 31),
problem4=(2, 36), problem5=(2, 50), problem6=(4, 8), problem4=(2, 36), problem5=(2, 50), problem6=(4, 8),
problem7=(5, 11), problem8=(8, 13)) 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' ' require-starting-space: yes\n'
' min-spaces-from-content: 2\n') ' min-spaces-from-content: 2\n')
self.check('# comment\n', conf) 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' self.check('---\n'
'obj1:\n' 'obj1:\n'
' a: 1\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' '\n'
'obj2:\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' self.check('---\n'
'obj1:\n' 'obj1:\n'
' a: 1\n' ' a: 1\n'
' # comments\n' ' # comments\n'
' b: 2\n', conf) ' 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): def test_first_line(self):
conf = 'comments-indentation: {}' conf = 'comments-indentation: {}'

View File

@@ -18,10 +18,32 @@ import unittest
import yaml 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): 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): def check_comments(self, buffer, *expected):
yaml_loader = yaml.BaseLoader(buffer) yaml_loader = yaml.BaseLoader(buffer)

View File

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

View File

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

View File

@@ -48,37 +48,205 @@ class IndentationTestCase(RuleTestCase):
'...\n', conf) '...\n', conf)
def test_one_space(self): 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' self.check('---\n'
'object:\n' 'object:\n'
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
' - b\n' ' - b\n'
' k2: v2\n' ' k2: v2\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf) '...\n', conf)
def test_two_spaces(self): 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' self.check('---\n'
'object:\n' 'object:\n'
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
' - b\n' ' - b\n'
' k2: v2\n' ' k2: v2\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf) '...\n', conf)
def test_three_spaces(self): 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' self.check('---\n'
'object:\n' 'object:\n'
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
' - b\n' ' - b\n'
' k2: v2\n' ' k2: v2\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf) '...\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}' 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' self.check('---\n'
'object:\n' 'object:\n'
' val: 1\n' ' val: 1\n'
@@ -88,7 +256,13 @@ class IndentationTestCase(RuleTestCase):
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
'...\n', conf, problem=(4, 4)) '...\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' self.check('---\n'
'object:\n' 'object:\n'
' val: 1\n' ' val: 1\n'
@@ -98,9 +272,15 @@ class IndentationTestCase(RuleTestCase):
'- el2:\n' '- el2:\n'
' - subel\n' ' - subel\n'
'...\n', conf, problem=(4, 4)) '...\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): def test_over_indented(self):
conf = 'indentation: {spaces: 2}' conf = 'indentation: {spaces: 2, indent-sequences: yes}'
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' val: 1\n' ' val: 1\n'
@@ -110,7 +290,13 @@ class IndentationTestCase(RuleTestCase):
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
'...\n', conf, problem=(4, 6)) '...\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' self.check('---\n'
'object:\n' 'object:\n'
' val: 1\n' ' val: 1\n'
@@ -118,7 +304,7 @@ class IndentationTestCase(RuleTestCase):
self.check('---\n' self.check('---\n'
' object:\n' ' object:\n'
' val: 1\n' ' val: 1\n'
'...\n', conf, problem1=(2, 2), problem2=(3, 6)) '...\n', conf, problem=(2, 2))
self.check('---\n' self.check('---\n'
'- el1\n' '- el1\n'
'- el2:\n' '- el2:\n'
@@ -129,30 +315,57 @@ class IndentationTestCase(RuleTestCase):
'- el2:\n' '- el2:\n'
' - subel\n' ' - subel\n'
'...\n', conf, problem=(4, 15)) '...\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' self.check('---\n'
' - el1\n' ' - el1\n'
' - el2:\n' ' - el2:\n'
' - subel\n' ' - subel\n'
'...\n', conf, '...\n', conf,
problem1=(2, 3), problem2=(3, 3), problem3=(4, 5)) problem=(2, 3))
def test_multi_lines(self): def test_multi_lines(self):
conf = 'indentation: {spaces: 2, indent-sequences: yes}'
self.check('---\n' self.check('---\n'
'long_string: >\n' 'long_string: >\n'
' bla bla blah\n' ' bla bla blah\n'
' blah bla bla\n' ' blah bla bla\n'
'...\n', None) '...\n', conf)
self.check('---\n' self.check('---\n'
'- long_string: >\n' '- long_string: >\n'
' bla bla blah\n' ' bla bla blah\n'
' blah bla bla\n' ' blah bla bla\n'
'...\n', None) '...\n', conf)
self.check('---\n' self.check('---\n'
'obj:\n' 'obj:\n'
' - long_string: >\n' ' - long_string: >\n'
' bla bla blah\n' ' bla bla blah\n'
' blah bla bla\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): def test_nested_collections(self):
conf = 'indentation: {spaces: 2}' conf = 'indentation: {spaces: 2}'
@@ -163,7 +376,7 @@ class IndentationTestCase(RuleTestCase):
self.check('---\n' self.check('---\n'
'- o:\n' '- o:\n'
' k1: v1\n' ' k1: v1\n'
'...\n', conf, problem=(3, 2)) '...\n', conf, problem=(3, 2, 'syntax'))
self.check('---\n' self.check('---\n'
'- o:\n' '- o:\n'
' k1: v1\n' ' k1: v1\n'
@@ -171,18 +384,33 @@ class IndentationTestCase(RuleTestCase):
conf = 'indentation: {spaces: 4}' conf = 'indentation: {spaces: 4}'
self.check('---\n' self.check('---\n'
'- o:\n' '- o:\n'
' k1: v1\n' ' k1: v1\n'
'...\n', conf) '...\n', conf)
self.check('---\n'
'- o:\n'
' k1: v1\n'
'...\n', conf, problem=(3, 4))
self.check('---\n' self.check('---\n'
'- o:\n' '- o:\n'
' k1: v1\n' ' k1: v1\n'
'...\n', conf, problem=(3, 6)) '...\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): def test_return(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n' self.check('---\n'
'a:\n' 'a:\n'
' b:\n' ' b:\n'
@@ -191,19 +419,19 @@ class IndentationTestCase(RuleTestCase):
' e:\n' ' e:\n'
' f:\n' ' f:\n'
'g:\n' 'g:\n'
'...\n', None) '...\n', conf)
# self.check('---\n' self.check('---\n'
# 'a:\n' 'a:\n'
# ' b:\n' ' b:\n'
# ' c:\n' ' c:\n'
# ' d:\n' ' d:\n'
# '...\n', None, problem=(5, 5)) '...\n', conf, problem=(5, 4, 'syntax'))
# self.check('---\n' self.check('---\n'
# 'a:\n' 'a:\n'
# ' b:\n' ' b:\n'
# ' c:\n' ' c:\n'
# ' d:\n' ' d:\n'
# '...\n', None, problem=(5, 2)) '...\n', conf, problem=(5, 2, 'syntax'))
def test_first_line(self): def test_first_line(self):
conf = ('indentation: {spaces: 2}\n' conf = ('indentation: {spaces: 2}\n'
@@ -224,3 +452,270 @@ class IndentationTestCase(RuleTestCase):
' 2,\n' ' 2,\n'
' 3\n' ' 3\n'
']\n', conf, problem1=(4, 4), problem2=(5, 2)) ']\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' '%TAG ! tag:clarkevans.com,2002:\n'
'doc: ument\n' 'doc: ument\n'
'...\n', None, problem=(3, 1)) '...\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('', conf)
self.check('\n', conf) self.check('\n', conf)
self.check(' \n', conf, problem=(1, 1)) 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' self.check('---\n'
'some: text \n', conf, problem=(2, 11)) 'some: text \n', conf, problem=(2, 11))
self.check('---\n' 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): def test_with_dos_new_lines(self):
conf = ('trailing-spaces: {}\n' 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_NAME = 'yamllint'
APP_VERSION = '0.2.0' APP_VERSION = '0.4.0'
APP_DESCRIPTION = 'A linter for YAML files.' APP_DESCRIPTION = 'A linter for YAML files.'
__author__ = 'Adrien Vergé' __author__ = 'Adrien Vergé'
@@ -38,12 +38,17 @@ def get_costemic_problems(buffer, conf):
token_rules = [r for r in rules if r.TYPE == 'token'] token_rules = [r for r in rules if r.TYPE == 'token']
line_rules = [r for r in rules if r.TYPE == 'line'] 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): for elem in parser.token_or_line_generator(buffer):
if isinstance(elem, parser.Token): if isinstance(elem, parser.Token):
for rule in token_rules: for rule in token_rules:
rule_conf = conf[rule.ID] rule_conf = conf[rule.ID]
for problem in rule.check(rule_conf, 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.rule = rule.ID
problem.level = rule_conf['level'] problem.level = rule_conf['level']
yield problem yield problem
@@ -58,7 +63,7 @@ def get_costemic_problems(buffer, conf):
def get_syntax_error(buffer): def get_syntax_error(buffer):
try: try:
list(yaml.safe_load_all(buffer)) list(yaml.parse(buffer, Loader=yaml.BaseLoader))
except yaml.error.MarkedYAMLError as e: except yaml.error.MarkedYAMLError as e:
problem = LintProblem(e.problem_mark.line + 1, problem = LintProblem(e.problem_mark.line + 1,
e.problem_mark.column + 1, e.problem_mark.column + 1,
@@ -76,11 +81,16 @@ def _lint(buffer, conf):
# Insert the syntax error (if any) at the right place... # Insert the syntax error (if any) at the right place...
if (syntax_error and syntax_error.line <= problem.line and if (syntax_error and syntax_error.line <= problem.line and
syntax_error.column <= problem.column): syntax_error.column <= problem.column):
# ... unless there is already a yamllint error, in which case the yield syntax_error
# syntax error is probably redundant.
if (syntax_error.line != problem.line or # If there is already a yamllint error at the same place, discard
syntax_error.column != problem.column): # it as it is probably redundant (and maybe it's just a 'warning',
yield syntax_error # 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 syntax_error = None
yield problem 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 max-spaces-after: 1
indentation: indentation:
spaces: 2 spaces: 2
indent-sequences: yes
line-length: line-length:
max: 80 max: 80
new-line-at-end-of-file: {level: error} new-line-at-end-of-file: {level: error}
new-lines: new-lines:
type: unix type: unix
#sequences-indentation:
# level: warning
# present: yes
trailing-spaces: {} trailing-spaces: {}

View File

@@ -46,7 +46,11 @@ def extend_config(content):
if 'extends' in conf: if 'extends' in conf:
base = parse_config_from_file(get_extended_conf(conf['extends'])) 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 conf['rules'] = base
return conf return conf
@@ -83,10 +87,16 @@ def parse_config(content):
raise YamlLintConfigError( raise YamlLintConfigError(
'invalid config: unknown option "%s" for rule "%s"' % 'invalid config: unknown option "%s" for rule "%s"' %
(optkey, id)) (optkey, id))
if type(conf['rules'][id][optkey]) != options[optkey]: if type(options[optkey]) == tuple:
raise YamlLintConfigError( if conf['rules'][id][optkey] not in options[optkey]:
'invalid config: option "%s" of "%s" should be %s' % raise YamlLintConfigError(
(optkey, id, options[optkey].__name__)) ('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] rules[id][optkey] = conf['rules'][id][optkey]
else: else:
raise YamlLintConfigError(('invalid config: rule "%s": should be ' 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} 'max-spaces-inside': int}
def check(conf, token, prev, next): def check(conf, token, prev, next, context):
if isinstance(token, yaml.FlowMappingStartToken): if isinstance(token, yaml.FlowMappingStartToken):
problem = spaces_after(token, prev, next, problem = spaces_after(token, prev, next,
min=conf['min-spaces-inside'], min=conf['min-spaces-inside'],

View File

@@ -25,7 +25,7 @@ CONF = {'min-spaces-inside': int,
'max-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): if isinstance(token, yaml.FlowSequenceStartToken):
problem = spaces_after(token, prev, next, problem = spaces_after(token, prev, next,
min=conf['min-spaces-inside'], min=conf['min-spaces-inside'],

View File

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

View File

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

View File

@@ -17,27 +17,48 @@
import yaml import yaml
from yamllint.errors import LintProblem 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' ID = 'comments-indentation'
TYPE = 'token' 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: if prev is None:
return return
token_indent = token.start_mark.column curr_line_indent = token.start_mark.column
if isinstance(token, yaml.StreamEndToken): if isinstance(token, yaml.StreamEndToken):
token_indent = 0 curr_line_indent = 0
skip_first = True skip_first_line = True
if isinstance(prev, yaml.StreamStartToken): 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, if prev_line_indent <= curr_line_indent:
skip_first_line=skip_first): prev_line_indent = -1 # disable it
if comment.column != token_indent + 1:
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, 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, def spaces_before(token, prev, next, min=-1, max=-1,
min_desc=None, max_desc=None): 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 spaces = token.start_mark.pointer - prev.end_mark.pointer
if max != - 1 and spaces > max: if max != - 1 and spaces > max:
return LintProblem(token.start_mark.line + 1, return LintProblem(token.start_mark.line + 1,
@@ -64,6 +67,16 @@ class Comment(object):
str(self) == str(other)) 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): def get_comments_between_tokens(token1, token2, skip_first_line=False):
if token2 is None: if token2 is None:
buf = token1.end_mark.buffer[token1.end_mark.pointer:] 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 pointer += len(line) + 1
line_no += 1 line_no += 1
column_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} CONF = {'present': bool}
def check(conf, token, prev, next): def check(conf, token, prev, next, context):
if conf['present']: if conf['present']:
if (isinstance(token, yaml.StreamEndToken) and if (isinstance(token, yaml.StreamEndToken) and
not (isinstance(prev, yaml.DocumentEndToken) or not (isinstance(prev, yaml.DocumentEndToken) or

View File

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

View File

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

View File

@@ -17,54 +17,247 @@
import yaml import yaml
from yamllint.errors import LintProblem from yamllint.errors import LintProblem
from yamllint.rules.common import is_explicit_key
ID = 'indentation' ID = 'indentation'
TYPE = 'token' 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): class Parent(object):
if isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)): 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 return
# Check if first token in line if token.plain:
if (not isinstance(prev, (yaml.StreamStartToken, yaml.DirectiveToken)) and expected_indent = token.start_mark.column
token.start_mark.line == prev.end_mark.line): elif token.style in ('"', "'"):
return 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: line_no = token.start_mark.line + 1
yield LintProblem(
token.end_mark.line + 1, token.start_mark.column + 1,
'indentation is not a multiple of %d' % conf['spaces'])
return
if isinstance(prev, (yaml.StreamStartToken, line_start = token.start_mark.pointer
yaml.DirectiveToken, while True:
yaml.DocumentStartToken, line_start = token.start_mark.buffer.find(
yaml.DocumentEndToken)): '\n', line_start, token.end_mark.pointer - 1) + 1
indent = 0 if line_start == 0:
else: break
buffer = prev.end_mark.buffer line_no += 1
start = buffer.rfind('\n', 0, prev.end_mark.pointer) + 1
indent = 0 indent = 0
while buffer[start + indent] == ' ': while token.start_mark.buffer[line_start + indent] == ' ':
indent += 1 indent += 1
if token.start_mark.buffer[line_start + indent] == '\n':
continue
if token.start_mark.column > indent: if indent != expected_indent:
if not isinstance(prev, (yaml.BlockSequenceStartToken, yield LintProblem(line_no, indent + 1,
yaml.BlockMappingStartToken, 'wrong indentation: expected %d but found %d' %
yaml.FlowSequenceStartToken, (expected_indent, indent))
yaml.FlowMappingStartToken,
yaml.KeyToken,
yaml.ValueToken)):
yield LintProblem(
token.end_mark.line + 1, token.start_mark.column + 1,
'unexpected indentation')
elif token.start_mark.column != indent + conf['spaces']:
yield LintProblem( def check(conf, token, prev, next, context):
token.end_mark.line + 1, token.start_mark.column + 1, if 'stack' not in context:
'found indentation of %d instead of %d' % context['stack'] = [Parent(ROOT, 0)]
(token.start_mark.column, indent + conf['spaces'])) 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))