diff --git a/CHANGELOG.rst b/CHANGELOG.rst index de0e50c..105fccd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog ========= +1.16.0 (2019-06-07) +------------------- + +- Add FreeBSD installation instructions +- Fix the ``line`` rule to correctly handle DOS new lines +- Add the ``allowed-values`` option to the ``truthy`` rule +- Allow configuration options to be a list of enums + 1.15.0 (2019-02-11) ------------------- diff --git a/README.rst b/README.rst index 4314f09..7f59956 100644 --- a/README.rst +++ b/README.rst @@ -51,6 +51,13 @@ On Debian 8+ / Ubuntu 16.04+: sudo apt-get install yamllint +On FreeBSD: + +.. code:: sh + + pkg install py27-yamllint + pkg install py36-yamllint + On Mac OS 10.11+: .. code:: bash diff --git a/setup.py b/setup.py index 1b93008..ffa2ee2 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from setuptools import setup, find_packages +from setuptools import find_packages, setup from yamllint import (__author__, __license__, APP_NAME, APP_VERSION, APP_DESCRIPTION) diff --git a/tests/rules/test_empty_lines.py b/tests/rules/test_empty_lines.py index b44a8a5..ab17d71 100644 --- a/tests/rules/test_empty_lines.py +++ b/tests/rules/test_empty_lines.py @@ -78,3 +78,22 @@ class EmptyLinesTestCase(RuleTestCase): 'document-start: disable\n') self.check('non empty\n', conf) self.check('non empty\n\n', conf, problem=(2, 1)) + + def test_with_dos_newlines(self): + conf = ('empty-lines: {max: 2, max-start: 0, max-end: 0}\n' + 'new-lines: {type: dos}\n' + 'document-start: disable\n') + self.check('---\r\n', conf) + self.check('---\r\ntext\r\n\r\ntext\r\n', conf) + self.check('\r\n---\r\ntext\r\n\r\ntext\r\n', conf, + problem=(1, 1)) + self.check('\r\n\r\n\r\n---\r\ntext\r\n\r\ntext\r\n', conf, + problem=(3, 1)) + self.check('---\r\ntext\r\n\r\n\r\n\r\ntext\r\n', conf, + problem=(5, 1)) + self.check('---\r\ntext\r\n\r\n\r\n\r\n\r\n\r\n\r\ntext\r\n', conf, + problem=(8, 1)) + self.check('---\r\ntext\r\n\r\ntext\r\n\r\n', conf, + problem=(5, 1)) + self.check('---\r\ntext\r\n\r\ntext\r\n\r\n\r\n\r\n', conf, + problem=(7, 1)) diff --git a/tests/rules/test_line_length.py b/tests/rules/test_line_length.py index 216317c..c1865f1 100644 --- a/tests/rules/test_line_length.py +++ b/tests/rules/test_line_length.py @@ -171,3 +171,12 @@ class LineLengthTestCase(RuleTestCase): '# This is a test to check if “line-length” works nice\n' 'with: “unicode characters” that span accross bytes! ↺\n', conf, problem1=(2, 53), problem2=(3, 53)) + + def test_with_dos_newlines(self): + conf = ('line-length: {max: 10}\n' + 'new-lines: {type: dos}\n' + 'new-line-at-end-of-file: disable\n') + self.check('---\r\nABCD EFGHI', conf) + self.check('---\r\nABCD EFGHI\r\n', conf) + self.check('---\r\nABCD EFGHIJ', conf, problem=(2, 11)) + self.check('---\r\nABCD EFGHIJ\r\n', conf, problem=(2, 11)) diff --git a/tests/rules/test_new_lines.py b/tests/rules/test_new_lines.py index c77a5d7..a76d608 100644 --- a/tests/rules/test_new_lines.py +++ b/tests/rules/test_new_lines.py @@ -31,16 +31,20 @@ class NewLinesTestCase(RuleTestCase): self.check('---\r\ntext\r\n', conf) def test_unix_type(self): - conf = 'new-lines: {type: unix}' + conf = ('new-line-at-end-of-file: disable\n' + 'new-lines: {type: unix}\n') self.check('', conf) + self.check('\r', conf) self.check('\n', conf) self.check('\r\n', conf, problem=(1, 1)) self.check('---\ntext\n', conf) self.check('---\r\ntext\r\n', conf, problem=(1, 4)) def test_dos_type(self): - conf = 'new-lines: {type: dos}\n' + conf = ('new-line-at-end-of-file: disable\n' + 'new-lines: {type: dos}\n') self.check('', conf) + self.check('\r', conf) self.check('\n', conf, problem=(1, 1)) self.check('\r\n', conf) self.check('---\ntext\n', conf, problem=(1, 4)) diff --git a/tests/rules/test_truthy.py b/tests/rules/test_truthy.py index b0402ed..82e4f6c 100644 --- a/tests/rules/test_truthy.py +++ b/tests/rules/test_truthy.py @@ -49,6 +49,54 @@ class TruthyTestCase(RuleTestCase): problem3=(7, 3), problem4=(7, 7), problem5=(8, 3), problem6=(8, 7)) + def test_different_allowed_values(self): + conf = ('truthy:\n' + ' allowed-values: ["yes", "no"]\n') + self.check('---\n' + 'key1: foo\n' + 'key2: yes\n' + 'key3: bar\n' + 'key4: no\n', conf) + self.check('---\n' + 'key1: true\n' + 'key2: Yes\n' + 'key3: false\n' + 'key4: no\n' + 'key5: yes\n', + conf, + problem1=(2, 7), problem2=(3, 7), + problem3=(4, 7)) + + def test_combined_allowed_values(self): + conf = ('truthy:\n' + ' allowed-values: ["yes", "no", "true", "false"]\n') + self.check('---\n' + 'key1: foo\n' + 'key2: yes\n' + 'key3: bar\n' + 'key4: no\n', conf) + self.check('---\n' + 'key1: true\n' + 'key2: Yes\n' + 'key3: false\n' + 'key4: no\n' + 'key5: yes\n', + conf, problem1=(3, 7)) + + def test_no_allowed_values(self): + conf = ('truthy:\n' + ' allowed-values: []\n') + self.check('---\n' + 'key1: foo\n' + 'key2: bar\n', conf) + self.check('---\n' + 'key1: true\n' + 'key2: yes\n' + 'key3: false\n' + 'key4: no\n', conf, + problem1=(2, 7), problem2=(3, 7), + problem3=(4, 7), problem4=(5, 7)) + def test_explicit_types(self): conf = 'truthy: enable\n' self.check('---\n' diff --git a/tests/test_config.py b/tests/test_config.py index f5d9402..d676382 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -174,6 +174,25 @@ class SimpleConfigTestCase(unittest.TestCase): self.assertRaises(config.YamlLintConfigError, config.validate_rule_conf, Rule, {'choice': 'abc'}) + Rule.CONF = {'multiple': ['item1', 'item2', 'item3']} + Rule.DEFAULT = {'multiple': ['item1']} + config.validate_rule_conf(Rule, {'multiple': []}) + config.validate_rule_conf(Rule, {'multiple': ['item2']}) + config.validate_rule_conf(Rule, {'multiple': ['item2', 'item3']}) + config.validate_rule_conf(Rule, {}) + self.assertRaises(config.YamlLintConfigError, + config.validate_rule_conf, Rule, + {'multiple': 'item1'}) + self.assertRaises(config.YamlLintConfigError, + config.validate_rule_conf, Rule, + {'multiple': ['']}) + self.assertRaises(config.YamlLintConfigError, + config.validate_rule_conf, Rule, + {'multiple': ['item1', 4]}) + self.assertRaises(config.YamlLintConfigError, + config.validate_rule_conf, Rule, + {'multiple': ['item4']}) + class ExtendedConfigTestCase(unittest.TestCase): def test_extend_on_object(self): diff --git a/yamllint/__init__.py b/yamllint/__init__.py index 91c19eb..932054e 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -22,7 +22,7 @@ indentation, etc.""" APP_NAME = 'yamllint' -APP_VERSION = '1.15.0' +APP_VERSION = '1.16.0' APP_DESCRIPTION = __doc__ __author__ = u'Adrien Vergé' diff --git a/yamllint/cli.py b/yamllint/cli.py index c5916e5..9975b4e 100644 --- a/yamllint/cli.py +++ b/yamllint/cli.py @@ -16,15 +16,15 @@ from __future__ import print_function +import argparse import os -import sys import platform -import argparse +import sys from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION +from yamllint import linter from yamllint.config import YamlLintConfig, YamlLintConfigError from yamllint.linter import PROBLEM_LEVELS -from yamllint import linter def find_files_recursively(items): diff --git a/yamllint/config.py b/yamllint/config.py index b4a7c1d..2cafe8a 100644 --- a/yamllint/config.py +++ b/yamllint/config.py @@ -134,12 +134,26 @@ def validate_rule_conf(rule, conf): raise YamlLintConfigError( 'invalid config: unknown option "%s" for rule "%s"' % (optkey, rule.ID)) + # Example: CONF = {option: (bool, 'mixed')} + # → {option: true} → {option: mixed} if isinstance(options[optkey], tuple): if (conf[optkey] not in options[optkey] and type(conf[optkey]) not in options[optkey]): raise YamlLintConfigError( 'invalid config: option "%s" of "%s" should be in %s' % (optkey, rule.ID, options[optkey])) + # Example: CONF = {option: ['flag1', 'flag2']} + # → {option: [flag1]} → {option: [flag1, flag2]} + elif isinstance(options[optkey], list): + if (type(conf[optkey]) is not list or + any(flag not in options[optkey] + for flag in conf[optkey])): + raise YamlLintConfigError( + ('invalid config: option "%s" of "%s" should only ' + 'contain values in %s') + % (optkey, rule.ID, str(options[optkey]))) + # Example: CONF = {option: int} + # → {option: 42} else: if not isinstance(conf[optkey], options[optkey]): raise YamlLintConfigError( diff --git a/yamllint/parser.py b/yamllint/parser.py index ae1ed5f..de331f4 100644 --- a/yamllint/parser.py +++ b/yamllint/parser.py @@ -77,7 +77,10 @@ def line_generator(buffer): cur = 0 next = buffer.find('\n') while next != -1: - yield Line(line_no, buffer, start=cur, end=next) + if next > 0 and buffer[next - 1] == '\r': + yield Line(line_no, buffer, start=cur, end=next - 1) + else: + yield Line(line_no, buffer, start=cur, end=next) cur = next + 1 next = buffer.find('\n', cur) line_no += 1 diff --git a/yamllint/rules/colons.py b/yamllint/rules/colons.py index fd46bef..1a63cad 100644 --- a/yamllint/rules/colons.py +++ b/yamllint/rules/colons.py @@ -72,7 +72,7 @@ Use this rule to control the number of spaces before and after colons (``:``). import yaml -from yamllint.rules.common import spaces_after, spaces_before, is_explicit_key +from yamllint.rules.common import is_explicit_key, spaces_after, spaces_before ID = 'colons' diff --git a/yamllint/rules/empty_lines.py b/yamllint/rules/empty_lines.py index 335b125..d9a8c4d 100644 --- a/yamllint/rules/empty_lines.py +++ b/yamllint/rules/empty_lines.py @@ -66,27 +66,37 @@ DEFAULT = {'max': 2, def check(conf, line): if line.start == line.end and line.end < len(line.buffer): # Only alert on the last blank line of a series - if (line.end < len(line.buffer) - 1 and - line.buffer[line.end + 1] == '\n'): + if (line.end + 2 <= len(line.buffer) and + line.buffer[line.end:line.end + 2] == '\n\n'): + return + elif (line.end + 4 <= len(line.buffer) and + line.buffer[line.end:line.end + 4] == '\r\n\r\n'): return blank_lines = 0 - while (line.start > blank_lines and - line.buffer[line.start - blank_lines - 1] == '\n'): + start = line.start + while start >= 2 and line.buffer[start - 2:start] == '\r\n': + blank_lines += 1 + start -= 2 + while start >= 1 and line.buffer[start - 1] == '\n': blank_lines += 1 + start -= 1 max = conf['max'] # Special case: start of document - if line.start - blank_lines == 0: + if start == 0: blank_lines += 1 # first line doesn't have a preceding \n max = conf['max-start'] # Special case: end of document # NOTE: The last line of a file is always supposed to end with a new # line. See POSIX definition of a line at: - if line.end == len(line.buffer) - 1 and line.buffer[line.end] == '\n': + if ((line.end == len(line.buffer) - 1 and + line.buffer[line.end] == '\n') or + (line.end == len(line.buffer) - 2 and + line.buffer[line.end:line.end + 2] == '\r\n')): # Allow the exception of the one-byte file containing '\n' if line.end == 0: return diff --git a/yamllint/rules/indentation.py b/yamllint/rules/indentation.py index 8bb7175..d83eb65 100644 --- a/yamllint/rules/indentation.py +++ b/yamllint/rules/indentation.py @@ -193,7 +193,7 @@ Use this rule to control the indentation. import yaml from yamllint.linter import LintProblem -from yamllint.rules.common import is_explicit_key, get_real_end_line +from yamllint.rules.common import get_real_end_line, is_explicit_key ID = 'indentation' diff --git a/yamllint/rules/new_lines.py b/yamllint/rules/new_lines.py index 3aae90f..686bac2 100644 --- a/yamllint/rules/new_lines.py +++ b/yamllint/rules/new_lines.py @@ -36,10 +36,11 @@ DEFAULT = {'type': 'unix'} def check(conf, line): if line.start == 0 and len(line.buffer) > line.end: if conf['type'] == 'dos': - if line.buffer[line.end - 1:line.end + 1] != '\r\n': + if (line.end + 2 > len(line.buffer) or + line.buffer[line.end:line.end + 2] != '\r\n'): yield LintProblem(1, line.end - line.start + 1, 'wrong new line character: expected \\r\\n') else: - if line.end > 0 and line.buffer[line.end - 1] == '\r': - yield LintProblem(1, line.end - line.start, + if line.buffer[line.end] == '\r': + yield LintProblem(1, line.end - line.start + 1, 'wrong new line character: expected \\n') diff --git a/yamllint/rules/truthy.py b/yamllint/rules/truthy.py index b738dc2..3cf058b 100644 --- a/yamllint/rules/truthy.py +++ b/yamllint/rules/truthy.py @@ -15,13 +15,22 @@ # along with this program. If not, see . """ -Use this rule to forbid non-explictly typed truthy values other than ``true`` -and ``false``, for example ``YES``, ``False`` and ``off``. +Use this rule to forbid non-explictly typed truthy values other than allowed +ones (by default: ``true`` and ``false``), for example ``YES`` or ``off``. This can be useful to prevent surprises from YAML parsers transforming ``[yes, FALSE, Off]`` into ``[true, false, false]`` or ``{y: 1, yes: 2, on: 3, true: 4, True: 5}`` into ``{y: 1, true: 5}``. +.. rubric:: Options + +* ``allowed-values`` defines the list of truthy values which will be ignored + during linting. The default is ``['true', 'false']``, but can be changed to + any list containing: ``'TRUE'``, ``'True'``, ``'true'``, ``'FALSE'``, + ``'False'``, ``'false'``, ``'YES'``, ``'Yes'``, ``'yes'``, ``'NO'``, + ``'No'``, ``'no'``, ``'ON'``, ``'On'``, ``'on'``, ``'OFF'``, ``'Off'``, + ``'off'``. + .. rubric:: Examples #. With ``truthy: {}`` @@ -63,29 +72,55 @@ This can be useful to prevent surprises from YAML parsers transforming yes: 1 on: 2 True: 3 + +#. With ``truthy: {allowed-values: ["yes", "no"]}`` + + the following code snippet would **PASS**: + :: + + - yes + - no + - "true" + - 'false' + - foo + - bar + + the following code snippet would **FAIL**: + :: + + - true + - false + - on + - off """ import yaml from yamllint.linter import LintProblem -ID = 'truthy' -TYPE = 'token' TRUTHY = ['YES', 'Yes', 'yes', 'NO', 'No', 'no', - 'TRUE', 'True', # 'true' is a boolean - 'FALSE', 'False', # 'false' is a boolean + 'TRUE', 'True', 'true', + 'FALSE', 'False', 'false', 'ON', 'On', 'on', 'OFF', 'Off', 'off'] +ID = 'truthy' +TYPE = 'token' +CONF = {'allowed-values': list(TRUTHY)} +DEFAULT = {'allowed-values': ['true', 'false']} + + def check(conf, token, prev, next, nextnext, context): if prev and isinstance(prev, yaml.tokens.TagToken): return if isinstance(token, yaml.tokens.ScalarToken): - if token.value in TRUTHY and token.style is None: + if (token.value in (set(TRUTHY) - set(conf['allowed-values'])) and + token.style is None): yield LintProblem(token.start_mark.line + 1, token.start_mark.column + 1, - "truthy value should be true or false") + "truthy value should be one of [" + + ", ".join(sorted(conf['allowed-values'])) + "]")