diff --git a/docs/configuration.rst b/docs/configuration.rst
index 18258d0..a42ec35 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -243,9 +243,9 @@ It is possible to set the ``locale`` option globally. This is passed to Python's
so an empty string ``""`` will use the system default locale, while e.g.
``"en_US.UTF-8"`` will use that.
-Currently this only affects the ``key-ordering`` rule. The default will order
-by Unicode code point number, while locales will sort case and accents
-properly as well.
+Currently this only affects the ``key-ordering`` and ``list-ordering`` rules.
+The default will order by Unicode code point number, while locales will sort
+case and accents properly as well.
.. code-block:: yaml
diff --git a/docs/rules.rst b/docs/rules.rst
index c030c3d..2738b32 100644
--- a/docs/rules.rst
+++ b/docs/rules.rst
@@ -95,6 +95,11 @@ line-length
.. automodule:: yamllint.rules.line_length
+list-ordering
+--------------
+
+.. automodule:: yamllint.rules.list_ordering
+
new-line-at-end-of-file
-----------------------
diff --git a/tests/rules/test_list_ordering.py b/tests/rules/test_list_ordering.py
new file mode 100644
index 0000000..a955465
--- /dev/null
+++ b/tests/rules/test_list_ordering.py
@@ -0,0 +1,133 @@
+# Copyright (C) 2023 Johannes F. Knauf and Kevin Wojniak
+#
+# 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 .
+
+import locale
+
+from tests.common import RuleTestCase
+
+
+class ListOrderingTestCase(RuleTestCase):
+ rule_id = 'list-ordering'
+
+ def test_disabled(self):
+ conf = 'list-ordering: disable'
+ self.check('---\n'
+ '- seconditem\n'
+ '- firstitem\n', conf)
+ self.check('---\n'
+ '[seconditem, firstitem]\n', conf)
+ self.check('---\n'
+ '- second\n'
+ '- at\n', conf)
+
+ def test_enabled(self):
+ conf = 'list-ordering: enable'
+ self.check('---\n'
+ '- seconditem\n'
+ '- firstitem\n', conf,
+ problem=(3, 3))
+ self.check('---\n'
+ '[seconditem, firstitem]\n', conf,
+ problem=(2, 14))
+ self.check('---\n'
+ '- second\n'
+ '- at\n', conf,
+ problem=(3, 3))
+ self.check('---\n'
+ 'nested but OK:\n'
+ ' - third: [second]\n'
+ ' - first\n', conf)
+ self.check('---\n'
+ 'nested failure:\n'
+ ' - third\n'
+ ' - first:\n'
+ ' items:\n'
+ ' - z\n'
+ ' - a\n', conf,
+ problem=(7, 9))
+
+ def test_word_length(self):
+ conf = 'list-ordering: enable'
+ self.check('---\n'
+ '- a\n'
+ '- ab\n'
+ '- abc\n', conf)
+ self.check('---\n'
+ '- a\n'
+ '- abc\n'
+ '- ab\n', conf,
+ problem=(4, 3))
+
+ def test_case(self):
+ conf = 'list-ordering: enable'
+ self.check('---\n'
+ '- T-shirt\n'
+ '- T-shirts\n'
+ '- t-shirt\n'
+ '- t-shirts\n', conf)
+ self.check('---\n'
+ '- T-shirt\n'
+ '- t-shirt\n'
+ '- T-shirts\n'
+ '- t-shirts\n', conf,
+ problem=(4, 3))
+
+ def test_accents(self):
+ conf = 'list-ordering: enable'
+ self.check('---\n'
+ '- hair\n'
+ '- hais\n'
+ '- haïr\n'
+ '- haïssable\n', conf)
+ self.check('---\n'
+ '- haïr\n'
+ '- hais\n', conf,
+ problem=(3, 3))
+
+ def test_locale_case(self):
+ self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
+ try:
+ locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
+ except locale.Error: # pragma: no cover
+ self.skipTest('locale en_US.UTF-8 not available')
+ conf = ('list-ordering: enable')
+ self.check('---\n'
+ '- t-shirt\n'
+ '- T-shirt\n'
+ '- t-shirts\n'
+ '- T-shirts\n', conf)
+ self.check('---\n'
+ '- t-shirt\n'
+ '- t-shirts\n'
+ '- T-shirt\n'
+ '- T-shirts\n', conf,
+ problem=(4, 3))
+
+ def test_locale_accents(self):
+ self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
+ try:
+ locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
+ except locale.Error: # pragma: no cover
+ self.skipTest('locale en_US.UTF-8 not available')
+ conf = ('list-ordering: enable')
+ self.check('---\n'
+ '- hair\n'
+ '- haïr\n'
+ '- hais\n'
+ '- haïssable\n', conf)
+ self.check('---\n'
+ '- hais\n'
+ '- haïr\n', conf,
+ problem=(3, 3))
diff --git a/yamllint/conf/default.yaml b/yamllint/conf/default.yaml
index 0dea0aa..b80887f 100644
--- a/yamllint/conf/default.yaml
+++ b/yamllint/conf/default.yaml
@@ -25,6 +25,7 @@ rules:
key-duplicates: enable
key-ordering: disable
line-length: enable
+ list-ordering: disable
new-line-at-end-of-file: enable
new-lines: enable
octal-values: disable
diff --git a/yamllint/rules/__init__.py b/yamllint/rules/__init__.py
index 5a4fe81..bd88d15 100644
--- a/yamllint/rules/__init__.py
+++ b/yamllint/rules/__init__.py
@@ -29,6 +29,7 @@ from yamllint.rules import (
key_duplicates,
key_ordering,
line_length,
+ list_ordering,
new_line_at_end_of_file,
new_lines,
octal_values,
@@ -55,6 +56,7 @@ _RULES = {
key_duplicates.ID: key_duplicates,
key_ordering.ID: key_ordering,
line_length.ID: line_length,
+ list_ordering.ID: list_ordering,
new_line_at_end_of_file.ID: new_line_at_end_of_file,
new_lines.ID: new_lines,
octal_values.ID: octal_values,
diff --git a/yamllint/rules/list_ordering.py b/yamllint/rules/list_ordering.py
new file mode 100644
index 0000000..5ff0c55
--- /dev/null
+++ b/yamllint/rules/list_ordering.py
@@ -0,0 +1,119 @@
+# Copyright (C) 2023 Johannes F. Knauf and Kevin Wojniak
+#
+# 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 items in lists. The sorting
+order uses the Unicode code point number as a default. As a result, the
+ordering is case-sensitive and not accent-friendly (see examples below).
+This can be changed by setting the global ``locale`` option. This allows one
+to sort case and accents properly.
+
+.. rubric:: Examples
+
+#. With ``list-ordering: {}``
+
+ the following code snippets would **PASS**:
+ ::
+
+ - key 1
+ - key 2
+ - key 3
+
+ - [a, b, c]
+
+ - T-shirt
+ - T-shirts
+ - t-shirt
+ - t-shirts
+
+ - hair
+ - hais
+ - haïr
+ - haïssable
+
+ the following code snippets would **FAIL**:
+ ::
+
+ - key 2
+ - key 1
+
+ - [b, a]
+
+ - T-shirt
+ - t-shirt
+ - T-shirts
+ - t-shirts
+
+ - haïr
+ - hais
+
+#. With global option ``locale: "en_US.UTF-8"`` and rule ``list-ordering: {}``
+
+ as opposed to before, the following code snippets would now **PASS**:
+ ::
+
+ - t-shirt
+ - T-shirt
+ - t-shirts
+ - T-shirts
+
+ - hair
+ - haïr
+ - hais
+ - haïssable
+"""
+
+from locale import strcoll
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'list-ordering'
+TYPE = 'token'
+
+MAP, SEQ = range(2)
+
+
+class Parent:
+ def __init__(self, type):
+ self.type = type
+ self.items = []
+
+
+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.ScalarToken):
+ if len(context['stack']) > 0 and context['stack'][-1].type == SEQ:
+ if any(strcoll(token.value, item) < 0
+ for item in context['stack'][-1].items):
+ yield LintProblem(
+ token.start_mark.line + 1, token.start_mark.column + 1,
+ 'wrong list item order "%s"' % token.value)
+ else:
+ context['stack'][-1].items.append(token.value)