diff --git a/docs/rules.rst b/docs/rules.rst index d71d0ae..5fe875f 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -69,6 +69,11 @@ indentation .. automodule:: yamllint.rules.indentation +key-duplicates +-------------- + +.. automodule:: yamllint.rules.key_duplicates + line-length ----------- diff --git a/tests/rules/test_key_duplicates.py b/tests/rules/test_key_duplicates.py new file mode 100644 index 0000000..b49c0e7 --- /dev/null +++ b/tests/rules/test_key_duplicates.py @@ -0,0 +1,156 @@ +# -*- 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 . + +from tests.common import RuleTestCase + + +class KeyDuplicatesTestCase(RuleTestCase): + rule_id = 'key-duplicates' + + def test_disabled(self): + conf = 'key-duplicates: disable' + self.check('---\n' + 'block mapping:\n' + ' key: a\n' + ' otherkey: b\n' + ' key: c\n', conf) + self.check('---\n' + 'flow mapping:\n' + ' {key: a, otherkey: b, key: c}\n', conf) + self.check('---\n' + 'duplicated twice:\n' + ' - k: a\n' + ' ok: b\n' + ' k: c\n' + ' k: d\n', conf) + self.check('---\n' + 'duplicated twice:\n' + ' - {k: a, ok: b, k: c, k: d}\n', conf) + self.check('---\n' + 'multiple duplicates:\n' + ' a: 1\n' + ' b: 2\n' + ' c: 3\n' + ' d: 4\n' + ' d: 5\n' + ' b: 6\n', conf) + self.check('---\n' + 'multiple duplicates:\n' + ' {a: 1, b: 2, c: 3, d: 4, d: 5, b: 6}\n', conf) + self.check('---\n' + 'at: root\n' + 'multiple: times\n' + 'at: root\n', conf) + self.check('---\n' + 'nested but OK:\n' + ' a: {a: {a: 1}}\n' + ' b:\n' + ' b: 2\n' + ' c: 3\n', conf) + self.check('---\n' + 'nested duplicates:\n' + ' a: {a: 1, a: 1}\n' + ' b:\n' + ' c: 3\n' + ' d: 4\n' + ' d: 4\n' + ' b: 2\n', conf) + self.check('---\n' + 'duplicates with many styles: 1\n' + '"duplicates with many styles": 1\n' + '\'duplicates with many styles\': 1\n' + '? duplicates with many styles\n' + ': 1\n' + '? >-\n' + ' duplicates with\n' + ' many styles\n' + ': 1\n', conf) + + def test_enabled(self): + conf = 'key-duplicates: {}' + self.check('---\n' + 'block mapping:\n' + ' key: a\n' + ' otherkey: b\n' + ' key: c\n', conf, + problem=(5, 3)) + self.check('---\n' + 'flow mapping:\n' + ' {key: a, otherkey: b, key: c}\n', conf, + problem=(3, 25)) + self.check('---\n' + 'duplicated twice:\n' + ' - k: a\n' + ' ok: b\n' + ' k: c\n' + ' k: d\n', conf, + problem1=(5, 5), problem2=(6, 5)) + self.check('---\n' + 'duplicated twice:\n' + ' - {k: a, ok: b, k: c, k: d}\n', conf, + problem1=(3, 19), problem2=(3, 25)) + self.check('---\n' + 'multiple duplicates:\n' + ' a: 1\n' + ' b: 2\n' + ' c: 3\n' + ' d: 4\n' + ' d: 5\n' + ' b: 6\n', conf, + problem1=(7, 3), problem2=(8, 3)) + self.check('---\n' + 'multiple duplicates:\n' + ' {a: 1, b: 2, c: 3, d: 4, d: 5, b: 6}\n', conf, + problem1=(3, 28), problem2=(3, 34)) + self.check('---\n' + 'at: root\n' + 'multiple: times\n' + 'at: root\n', conf, + problem=(4, 1)) + self.check('---\n' + 'nested but OK:\n' + ' a: {a: {a: 1}}\n' + ' b:\n' + ' b: 2\n' + ' c: 3\n', conf) + self.check('---\n' + 'nested duplicates:\n' + ' a: {a: 1, a: 1}\n' + ' b:\n' + ' c: 3\n' + ' d: 4\n' + ' d: 4\n' + ' b: 2\n', conf, + problem1=(3, 13), problem2=(7, 5), problem3=(8, 3)) + self.check('---\n' + 'duplicates with many styles: 1\n' + '"duplicates with many styles": 1\n' + '\'duplicates with many styles\': 1\n' + '? duplicates with many styles\n' + ': 1\n' + '? >-\n' + ' duplicates with\n' + ' many styles\n' + ': 1\n', conf, + problem1=(3, 1), problem2=(4, 1), problem3=(5, 3), + problem4=(7, 3)) + + def test_key_tokens_in_flow_sequences(self): + conf = 'key-duplicates: {}' + self.check('---\n' + '[\n' + ' flow: sequence, with, key: value, mappings\n' + ']\n', conf) diff --git a/yamllint/conf/default.yml b/yamllint/conf/default.yml index 2b5aa3c..71bd825 100644 --- a/yamllint/conf/default.yml +++ b/yamllint/conf/default.yml @@ -34,6 +34,7 @@ rules: spaces: 2 indent-sequences: yes check-multi-line-strings: no + key-duplicates: {} line-length: max: 80 allow-non-breakable-words: yes diff --git a/yamllint/rules/__init__.py b/yamllint/rules/__init__.py index 8a7dd23..6e82970 100644 --- a/yamllint/rules/__init__.py +++ b/yamllint/rules/__init__.py @@ -26,6 +26,7 @@ from yamllint.rules import ( empty_lines, hyphens, indentation, + key_duplicates, line_length, new_line_at_end_of_file, new_lines, @@ -44,6 +45,7 @@ _RULES = { empty_lines.ID: empty_lines, hyphens.ID: hyphens, indentation.ID: indentation, + key_duplicates.ID: key_duplicates, 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_duplicates.py b/yamllint/rules/key_duplicates.py new file mode 100644 index 0000000..9f879f0 --- /dev/null +++ b/yamllint/rules/key_duplicates.py @@ -0,0 +1,99 @@ +# -*- 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 . + +""" +Use this rule to prevent multiple entries with the same key in mappings. + +.. rubric:: Examples + +#. With ``key-duplicates: {}`` + + 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 1: v + key 2: val + key 1: value + + the following code snippet would **FAIL**: + :: + + - {a: 1, b: 2, b: 3} + + the following code snippet would **FAIL**: + :: + + duplicated key: 1 + "duplicated key": 2 + + other duplication: 1 + ? >- + other + duplication + : 2 +""" + +import yaml + +from yamllint.linter import LintProblem + + +ID = 'key-duplicates' +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, 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 next.value in context['stack'][-1].keys: + yield LintProblem( + next.start_mark.line + 1, next.start_mark.column + 1, + 'duplication of key "%s" in mapping' % next.value) + else: + context['stack'][-1].keys.append(next.value)