Compare commits

..

8 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
11 changed files with 527 additions and 113 deletions

View File

@@ -14,12 +14,16 @@ yamllint my_file.yml my_other_file.yaml ...
```
```sh
yamllint -c ~/myconfig my_file.yml
yamllint .
```
```sh
yamllint -c ~/myconfig file.yml
```
```sh
# To output a format parsable (by editors like Vim, emacs, etc.)
yamllint -f parsable my_file.yml
yamllint -f parsable file.yml
```
## Installation

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

@@ -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}'

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

@@ -452,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

@@ -22,7 +22,7 @@ from yamllint import parser
APP_NAME = 'yamllint'
APP_VERSION = '0.3.0'
APP_VERSION = '0.4.0'
APP_DESCRIPTION = 'A linter for YAML files.'
__author__ = 'Adrien Vergé'

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

@@ -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

@@ -30,11 +30,13 @@ 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

@@ -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,

View File

@@ -35,6 +35,72 @@ class Parent(object):
self.explicit_key = False
def check_scalar_indentation(conf, token, context):
if token.start_mark.line == token.end_mark.line:
return
if token.plain:
expected_indent = token.start_mark.column
elif token.style in ('"', "'"):
expected_indent = token.start_mark.column + 1
elif token.style in ('>', '|'):
if context['stack'][-1].type == B_SEQ:
# - >
# multi
# line
expected_indent = token.start_mark.column + conf['spaces']
elif context['stack'][-1].type == KEY:
assert context['stack'][-1].explicit_key
# - ? >
# multi-line
# key
# : >
# multi-line
# value
expected_indent = token.start_mark.column + conf['spaces']
elif context['stack'][-1].type == VAL:
if token.start_mark.line + 1 > context['cur_line']:
# - key:
# >
# multi
# line
expected_indent = context['stack'][-1].indent + conf['spaces']
elif context['stack'][-2].explicit_key:
# - ? key
# : >
# multi-line
# value
expected_indent = token.start_mark.column + conf['spaces']
else:
# - key: >
# multi
# line
expected_indent = context['stack'][-2].indent + conf['spaces']
else:
expected_indent = context['stack'][-1].indent + conf['spaces']
line_no = token.start_mark.line + 1
line_start = token.start_mark.pointer
while True:
line_start = token.start_mark.buffer.find(
'\n', line_start, token.end_mark.pointer - 1) + 1
if line_start == 0:
break
line_no += 1
indent = 0
while token.start_mark.buffer[line_start + indent] == ' ':
indent += 1
if token.start_mark.buffer[line_start + indent] == '\n':
continue
if indent != expected_indent:
yield LintProblem(line_no, indent + 1,
'wrong indentation: expected %d but found %d' %
(expected_indent, indent))
def check(conf, token, prev, next, context):
if 'stack' not in context:
context['stack'] = [Parent(ROOT, 0)]
@@ -42,11 +108,13 @@ def check(conf, token, prev, next, context):
# Step 1: Lint
if (not isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)) and
not isinstance(token, yaml.BlockEndToken) and
not (isinstance(token, yaml.ScalarToken) and token.value == '') and
token.start_mark.line + 1 > context['cur_line']):
needs_lint = (
not isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)) and
not isinstance(token, yaml.BlockEndToken) and
not (isinstance(token, yaml.ScalarToken) and token.value == '') and
token.start_mark.line + 1 > context['cur_line'])
if needs_lint:
found_indentation = token.start_mark.column
expected = context['stack'][-1].indent
@@ -63,10 +131,17 @@ def check(conf, token, prev, next, context):
'wrong indentation: expected %d but found %d' %
(expected, found_indentation))
if isinstance(token, yaml.ScalarToken):
for problem in check_scalar_indentation(conf, token, context):
yield problem
# Step 2.a:
if needs_lint:
context['cur_line_indent'] = found_indentation
context['cur_line'] = token.end_mark.line + 1
# Step 2: Update state
# Step 2.b: Update state
if isinstance(token, yaml.BlockMappingStartToken):
assert isinstance(next, yaml.KeyToken)