From 1543d0e43556cd5165a7e785395964f9badfa404 Mon Sep 17 00:00:00 2001 From: "Johannes F. Knauf" Date: Tue, 19 Sep 2017 09:54:45 +0200 Subject: [PATCH] New rule key-ordering closes #67 --- docs/rules.rst | 5 ++ tests/rules/test_key_ordering.py | 67 +++++++++++++++++++++++++ yamllint/conf/default.yaml | 1 + yamllint/rules/__init__.py | 2 + yamllint/rules/key_ordering.py | 86 ++++++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 tests/rules/test_key_ordering.py create mode 100644 yamllint/rules/key_ordering.py 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_key_ordering.py b/tests/rules/test_key_ordering.py new file mode 100644 index 0000000..bcd9eef --- /dev/null +++ b/tests/rules/test_key_ordering.py @@ -0,0 +1,67 @@ +# -*- 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_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/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/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/key_ordering.py b/yamllint/rules/key_ordering.py new file mode 100644 index 0000000..4a7e31f --- /dev/null +++ b/yamllint/rules/key_ordering.py @@ -0,0 +1,86 @@ +# -*- 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. + +.. 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} + + the following code snippet would **FAIL**: + :: + + - key 2: v + key 1: val + + the following code snippet would **FAIL**: + :: + + - {b: 1, a: 2} +""" + +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)