diff --git a/hooks.yaml b/.pre-commit-hooks.yaml similarity index 89% rename from hooks.yaml rename to .pre-commit-hooks.yaml index 482f3fb..8c67058 100644 --- a/hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -8,4 +8,4 @@ description: This hook runs yamllint. entry: yamllint language: python - files: \.(yaml|yml)$ + types: [file, yaml] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0b2f9ff..73de95f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,19 @@ Changelog ========= +1.9.0 (2017-10-16) +------------------ + +- Add a new `key-ordering` rule +- Fix indentation rule for key following empty list + +1.8.2 (2017-10-10) +------------------ + +- Be clearer about the `ignore` conf type +- Update pre-commit hook file +- Add documentation for pre-commit + 1.8.1 (2017-07-04) ------------------ diff --git a/docs/conf.py b/docs/conf.py index 26668da..8225094 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,7 @@ import sys import os +from unittest.mock import MagicMock sys.path.insert(0, os.path.abspath('..')) # noqa @@ -40,3 +41,15 @@ htmlhelp_basename = 'yamllintdoc' man_pages = [ ('index', 'yamllint', '', [u'Adrien Vergé'], 1) ] + +# -- Build with sphinx automodule without needing to install third-party libs + + +class Mock(MagicMock): + @classmethod + def __getattr__(cls, name): + return MagicMock() + + +MOCK_MODULES = ['pathspec', 'yaml'] +sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) diff --git a/docs/index.rst b/docs/index.rst index 97e1fc7..958c951 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,3 +26,4 @@ Table of contents disable_with_comments development text_editors + integration diff --git a/docs/integration.rst b/docs/integration.rst new file mode 100644 index 0000000..da67d7f --- /dev/null +++ b/docs/integration.rst @@ -0,0 +1,17 @@ +Integration with other software +=============================== + +Integration with pre-commit +--------------------------- + +You can integrate yamllint in `pre-commit `_ tool. +Here is an example, to add in your .pre-commit-config.yaml + +.. code:: yaml + + --- + # Update the sha variable with the release version that you want, from the yamllint repo + - repo: https://github.com/adrienverge/yamllint.git + sha: v1.8.1 + hooks: + - id: yamllint diff --git a/docs/rules.rst b/docs/rules.rst index da744a0..dab310d 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -74,6 +74,11 @@ key-duplicates .. automodule:: yamllint.rules.key_duplicates +key-ordering +-------------- + +.. automodule:: yamllint.rules.key_ordering + line-length ----------- diff --git a/tests/rules/test_indentation.py b/tests/rules/test_indentation.py index 2733ea7..374f23c 100644 --- a/tests/rules/test_indentation.py +++ b/tests/rules/test_indentation.py @@ -589,6 +589,9 @@ class IndentationTestCase(RuleTestCase): ' date: 1969\n' ' - name: Linux\n' ' date: 1991\n' + ' k4:\n' + ' -\n' + ' k5: v3\n' '...\n', conf) conf = 'indentation: {spaces: 2, indent-sequences: true}' self.check('---\n' @@ -1208,6 +1211,20 @@ class IndentationTestCase(RuleTestCase): ' - name: Linux\n' ' date: 1991\n' '...\n', conf, problem=(5, 10, 'syntax')) + conf = 'indentation: {spaces: 2, indent-sequences: true}' + self.check('---\n' + 'a:\n' + '-\n' # empty list + 'b: c\n' + '...\n', conf, problem=(3, 1)) + conf = 'indentation: {spaces: 2, indent-sequences: consistent}' + self.check('---\n' + 'a:\n' + ' -\n' # empty list + 'b:\n' + '-\n' + 'c: d\n' + '...\n', conf, problem=(5, 1)) def test_over_indented(self): conf = 'indentation: {spaces: 2, indent-sequences: consistent}' @@ -1264,6 +1281,20 @@ class IndentationTestCase(RuleTestCase): ' - subel\n' '...\n', conf, problem=(2, 3)) + conf = 'indentation: {spaces: 2, indent-sequences: false}' + self.check('---\n' + 'a:\n' + ' -\n' # empty list + 'b: c\n' + '...\n', conf, problem=(3, 3)) + conf = 'indentation: {spaces: 2, indent-sequences: consistent}' + self.check('---\n' + 'a:\n' + '-\n' # empty list + 'b:\n' + ' -\n' + 'c: d\n' + '...\n', conf, problem=(5, 3)) def test_multi_lines(self): conf = 'indentation: {spaces: consistent, indent-sequences: true}' diff --git a/tests/rules/test_key_ordering.py b/tests/rules/test_key_ordering.py new file mode 100644 index 0000000..dc486af --- /dev/null +++ b/tests/rules/test_key_ordering.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 Johannes F. Knauf +# +# 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 . + +from tests.common import RuleTestCase + + +class KeyOrderingTestCase(RuleTestCase): + rule_id = 'key-ordering' + + def test_disabled(self): + conf = 'key-ordering: disable' + self.check('---\n' + 'block mapping:\n' + ' secondkey: a\n' + ' firstkey: b\n', conf) + self.check('---\n' + 'flow mapping:\n' + ' {secondkey: a, firstkey: b}\n', conf) + self.check('---\n' + 'second: before_first\n' + 'at: root\n', conf) + self.check('---\n' + 'nested but OK:\n' + ' second: {first: 1}\n' + ' third:\n' + ' second: 2\n', conf) + + def test_enabled(self): + conf = 'key-ordering: enable' + self.check('---\n' + 'block mapping:\n' + ' secondkey: a\n' + ' firstkey: b\n', conf, + problem=(4, 3)) + self.check('---\n' + 'flow mapping:\n' + ' {secondkey: a, firstkey: b}\n', conf, + problem=(3, 18)) + self.check('---\n' + 'second: before_first\n' + 'at: root\n', conf, + problem=(3, 1)) + self.check('---\n' + 'nested but OK:\n' + ' second: {first: 1}\n' + ' third:\n' + ' second: 2\n', conf) + + def test_word_length(self): + conf = 'key-ordering: enable' + self.check('---\n' + 'a: 1\n' + 'ab: 1\n' + 'abc: 1\n', conf) + self.check('---\n' + 'a: 1\n' + 'abc: 1\n' + 'ab: 1\n', conf, + problem=(4, 1)) + + def test_key_duplicates(self): + conf = ('key-duplicates: disable\n' + 'key-ordering: enable') + self.check('---\n' + 'key: 1\n' + 'key: 2\n', conf) + + def test_case(self): + conf = 'key-ordering: enable' + self.check('---\n' + 'T-shirt: 1\n' + 'T-shirts: 2\n' + 't-shirt: 3\n' + 't-shirts: 4\n', conf) + self.check('---\n' + 'T-shirt: 1\n' + 't-shirt: 2\n' + 'T-shirts: 3\n' + 't-shirts: 4\n', conf, + problem=(4, 1)) + + def test_accents(self): + conf = 'key-ordering: enable' + self.check('---\n' + 'hair: true\n' + 'hais: true\n' + 'haïr: true\n' + 'haïssable: true\n', conf) + self.check('---\n' + 'haïr: true\n' + 'hais: true\n', conf, + problem=(3, 1)) + self.check('---\n' + 'haïr: true\n' + 'hais: true\n', conf, + problem=(3, 1)) + + def test_key_tokens_in_flow_sequences(self): + conf = 'key-ordering: enable' + self.check('---\n' + '[\n' + ' key: value, mappings, in, flow: sequence\n' + ']\n', conf) diff --git a/yamllint/__init__.py b/yamllint/__init__.py index e53dce1..831da23 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -22,7 +22,7 @@ indentation, etc.""" APP_NAME = 'yamllint' -APP_VERSION = '1.8.1' +APP_VERSION = '1.9.0' APP_DESCRIPTION = __doc__ __author__ = u'Adrien Vergé' diff --git a/yamllint/conf/default.yaml b/yamllint/conf/default.yaml index c7c4da4..57cff64 100644 --- a/yamllint/conf/default.yaml +++ b/yamllint/conf/default.yaml @@ -39,6 +39,7 @@ rules: indent-sequences: true check-multi-line-strings: false key-duplicates: enable + key-ordering: disable line-length: max: 80 allow-non-breakable-words: true diff --git a/yamllint/config.py b/yamllint/config.py index fb5a161..f9e4de8 100644 --- a/yamllint/config.py +++ b/yamllint/config.py @@ -87,7 +87,7 @@ class YamlLintConfig(object): if 'ignore' in conf: if type(conf['ignore']) != str: raise YamlLintConfigError( - 'invalid config: ignore should be a list of patterns') + 'invalid config: ignore should contain file patterns') self.ignore = pathspec.PathSpec.from_lines( 'gitwildmatch', conf['ignore'].splitlines()) @@ -112,7 +112,7 @@ def validate_rule_conf(rule, conf): type(conf['ignore']) != pathspec.pathspec.PathSpec): if type(conf['ignore']) != str: raise YamlLintConfigError( - 'invalid config: ignore should be a list of patterns') + 'invalid config: ignore should contain file patterns') conf['ignore'] = pathspec.PathSpec.from_lines( 'gitwildmatch', conf['ignore'].splitlines()) diff --git a/yamllint/rules/__init__.py b/yamllint/rules/__init__.py index 619e32d..83dca76 100644 --- a/yamllint/rules/__init__.py +++ b/yamllint/rules/__init__.py @@ -27,6 +27,7 @@ from yamllint.rules import ( hyphens, indentation, key_duplicates, + key_ordering, line_length, new_line_at_end_of_file, new_lines, @@ -47,6 +48,7 @@ _RULES = { hyphens.ID: hyphens, indentation.ID: indentation, key_duplicates.ID: key_duplicates, + key_ordering.ID: key_ordering, line_length.ID: line_length, new_line_at_end_of_file.ID: new_line_at_end_of_file, new_lines.ID: new_lines, diff --git a/yamllint/rules/indentation.py b/yamllint/rules/indentation.py index 432c23c..fb14faf 100644 --- a/yamllint/rules/indentation.py +++ b/yamllint/rules/indentation.py @@ -399,6 +399,10 @@ def _check(conf, token, prev, next, nextnext, context): # - item 1 # - item 2 indent = next.start_mark.column + elif next.start_mark.column == token.start_mark.column: + # - + # key: value + indent = next.start_mark.column else: # - # item 1 diff --git a/yamllint/rules/key_ordering.py b/yamllint/rules/key_ordering.py new file mode 100644 index 0000000..3bd93c7 --- /dev/null +++ b/yamllint/rules/key_ordering.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 Johannes F. Knauf +# +# 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 . + +""" +Use this rule to enforce alphabetical ordering of keys in mappings. The sorting +order uses the Unicode code point number. As a result, the ordering is +case-sensitive and not accent-friendly (see examples below). + +.. rubric:: Examples + +#. With ``key-ordering: {}`` + + the following code snippet would **PASS**: + :: + + - key 1: v + key 2: val + key 3: value + - {a: 1, b: 2, c: 3} + - T-shirt: 1 + T-shirts: 2 + t-shirt: 3 + t-shirts: 4 + - hair: true + hais: true + haïr: true + haïssable: true + + the following code snippet would **FAIL**: + :: + + - key 2: v + key 1: val + + the following code snippet would **FAIL**: + :: + + - {b: 1, a: 2} + + the following code snippet would **FAIL**: + :: + + - T-shirt: 1 + t-shirt: 2 + T-shirts: 3 + t-shirts: 4 + + the following code snippet would **FAIL**: + :: + + - haïr: true + hais: true +""" + +import yaml + +from yamllint.linter import LintProblem + + +ID = 'key-ordering' +TYPE = 'token' +CONF = {} + +MAP, SEQ = range(2) + + +class Parent(object): + def __init__(self, type): + self.type = type + self.keys = [] + + +def check(conf, token, prev, next, nextnext, context): + if 'stack' not in context: + context['stack'] = [] + + if isinstance(token, (yaml.BlockMappingStartToken, + yaml.FlowMappingStartToken)): + context['stack'].append(Parent(MAP)) + elif isinstance(token, (yaml.BlockSequenceStartToken, + yaml.FlowSequenceStartToken)): + context['stack'].append(Parent(SEQ)) + elif isinstance(token, (yaml.BlockEndToken, + yaml.FlowMappingEndToken, + yaml.FlowSequenceEndToken)): + context['stack'].pop() + elif (isinstance(token, yaml.KeyToken) and + isinstance(next, yaml.ScalarToken)): + # This check is done because KeyTokens can be found inside flow + # sequences... strange, but allowed. + if len(context['stack']) > 0 and context['stack'][-1].type == MAP: + if any(next.value < key for key in context['stack'][-1].keys): + yield LintProblem( + next.start_mark.line + 1, next.start_mark.column + 1, + 'wrong ordering of key "%s" in mapping' % next.value) + else: + context['stack'][-1].keys.append(next.value)