summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.flake84
-rw-r--r--.pre-commit-hooks.yaml11
-rw-r--r--CHANGELOG.rst286
-rw-r--r--CONTRIBUTING.rst48
-rw-r--r--LICENSE674
-rw-r--r--README.rst140
-rw-r--r--docs/Makefile177
-rw-r--r--docs/conf.py54
-rw-r--r--docs/configuration.rst255
-rw-r--r--docs/development.rst18
-rw-r--r--docs/disable_with_comments.rst136
-rw-r--r--docs/index.rst29
-rw-r--r--docs/integration.rst67
-rw-r--r--docs/quickstart.rst115
-rw-r--r--docs/rules.rst131
-rw-r--r--docs/screenshot.pngbin0 -> 31372 bytes
-rw-r--r--docs/text_editors.rst52
-rw-r--r--pyproject.toml54
-rw-r--r--setup.py20
-rw-r--r--tests/__init__.py19
-rw-r--r--tests/common.py86
-rw-r--r--tests/rules/__init__.py0
-rw-r--r--tests/rules/test_anchors.py281
-rw-r--r--tests/rules/test_braces.py340
-rw-r--r--tests/rules/test_brackets.py337
-rw-r--r--tests/rules/test_colons.py274
-rw-r--r--tests/rules/test_commas.py264
-rw-r--r--tests/rules/test_comments.py236
-rw-r--r--tests/rules/test_comments_indentation.py156
-rw-r--r--tests/rules/test_common.py43
-rw-r--r--tests/rules/test_document_end.py92
-rw-r--r--tests/rules/test_document_start.py103
-rw-r--r--tests/rules/test_empty_lines.py98
-rw-r--r--tests/rules/test_empty_values.py368
-rw-r--r--tests/rules/test_float_values.py128
-rw-r--r--tests/rules/test_hyphens.py105
-rw-r--r--tests/rules/test_indentation.py2160
-rw-r--r--tests/rules/test_key_duplicates.py181
-rw-r--r--tests/rules/test_key_ordering.py149
-rw-r--r--tests/rules/test_line_length.py198
-rw-r--r--tests/rules/test_new_line_at_end_of_file.py41
-rw-r--r--tests/rules/test_new_lines.py96
-rw-r--r--tests/rules/test_octal_values.py80
-rw-r--r--tests/rules/test_quoted_strings.py558
-rw-r--r--tests/rules/test_trailing_spaces.py47
-rw-r--r--tests/rules/test_truthy.py145
-rw-r--r--tests/test_cli.py795
-rw-r--r--tests/test_config.py763
-rw-r--r--tests/test_linter.py66
-rw-r--r--tests/test_module.py84
-rw-r--r--tests/test_parser.py152
-rw-r--r--tests/test_spec_examples.py188
-rw-r--r--tests/test_syntax_errors.py93
-rw-r--r--tests/test_yamllint_directives.py432
-rw-r--r--tests/yaml-1.2-spec-examples/example-10.16
-rw-r--r--tests/yaml-1.2-spec-examples/example-10.26
-rw-r--r--tests/yaml-1.2-spec-examples/example-10.34
-rw-r--r--tests/yaml-1.2-spec-examples/example-10.42
-rw-r--r--tests/yaml-1.2-spec-examples/example-10.52
-rw-r--r--tests/yaml-1.2-spec-examples/example-10.63
-rw-r--r--tests/yaml-1.2-spec-examples/example-10.75
-rw-r--r--tests/yaml-1.2-spec-examples/example-10.85
-rw-r--r--tests/yaml-1.2-spec-examples/example-10.97
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.13
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.108
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.119
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.129
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.134
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.144
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.158
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.168
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.177
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.187
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.195
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.23
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.205
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.213
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.224
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.2314
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.2414
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.257
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.267
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.2729
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.2829
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.38
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.48
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.55
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.65
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.710
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.810
-rw-r--r--tests/yaml-1.2-spec-examples/example-2.98
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.11
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.102
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.113
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.126
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.135
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.143
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.23
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.37
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.42
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.51
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.62
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.76
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.82
-rw-r--r--tests/yaml-1.2-spec-examples/example-5.92
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.112
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.103
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.114
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.126
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.133
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.144
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.153
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.163
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.173
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.187
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.193
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.24
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.203
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.217
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.223
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.233
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.242
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.252
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.265
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.274
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.284
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.292
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.33
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.47
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.57
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.67
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.76
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.87
-rw-r--r--tests/yaml-1.2-spec-examples/example-6.92
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.14
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.1012
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.113
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.124
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.132
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.148
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.152
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.165
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.176
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.185
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.193
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.24
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.204
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.213
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.223
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.235
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.245
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.34
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.43
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.55
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.64
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.71
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.83
-rw-r--r--tests/yaml-1.2-spec-examples/example-7.94
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.110
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.1016
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.1116
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.1216
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.1315
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.143
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.156
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.162
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.175
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.184
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.193
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.211
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.206
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.216
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.226
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.38
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.46
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.519
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.66
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.74
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.89
-rw-r--r--tests/yaml-1.2-spec-examples/example-8.94
-rw-r--r--tests/yaml-1.2-spec-examples/example-9.13
-rw-r--r--tests/yaml-1.2-spec-examples/example-9.24
-rw-r--r--tests/yaml-1.2-spec-examples/example-9.37
-rw-r--r--tests/yaml-1.2-spec-examples/example-9.47
-rw-r--r--tests/yaml-1.2-spec-examples/example-9.58
-rw-r--r--tests/yaml-1.2-spec-examples/example-9.67
-rw-r--r--yamllint/__init__.py30
-rw-r--r--yamllint/__main__.py4
-rw-r--r--yamllint/cli.py249
-rw-r--r--yamllint/conf/default.yaml35
-rw-r--r--yamllint/conf/relaxed.yaml29
-rw-r--r--yamllint/config.py235
-rw-r--r--yamllint/linter.py236
-rw-r--r--yamllint/parser.py159
-rw-r--r--yamllint/rules/__init__.py73
-rw-r--r--yamllint/rules/anchors.py174
-rw-r--r--yamllint/rules/braces.py201
-rw-r--r--yamllint/rules/brackets.py203
-rw-r--r--yamllint/rules/colons.py115
-rw-r--r--yamllint/rules/commas.py140
-rw-r--r--yamllint/rules/comments.py113
-rw-r--r--yamllint/rules/comments_indentation.py137
-rw-r--r--yamllint/rules/common.py88
-rw-r--r--yamllint/rules/document_end.py116
-rw-r--r--yamllint/rules/document_start.py100
-rw-r--r--yamllint/rules/empty_lines.py117
-rw-r--r--yamllint/rules/empty_values.py140
-rw-r--r--yamllint/rules/float_values.py158
-rw-r--r--yamllint/rules/hyphens.py95
-rw-r--r--yamllint/rules/indentation.py587
-rw-r--r--yamllint/rules/key_duplicates.py100
-rw-r--r--yamllint/rules/key_ordering.py127
-rw-r--r--yamllint/rules/line_length.py158
-rw-r--r--yamllint/rules/new_line_at_end_of_file.py36
-rw-r--r--yamllint/rules/new_lines.py59
-rw-r--r--yamllint/rules/octal_values.py112
-rw-r--r--yamllint/rules/quoted_strings.py289
-rw-r--r--yamllint/rules/trailing_spaces.py61
-rw-r--r--yamllint/rules/truthy.py157
219 files changed, 16836 insertions, 0 deletions
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..2b7958b
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,4 @@
+[flake8]
+import-order-style = pep8
+application-import-names = yamllint
+ignore = W503,W504
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..6de9ed7
--- /dev/null
+++ b/.pre-commit-hooks.yaml
@@ -0,0 +1,11 @@
+---
+
+# For use with pre-commit.
+# See usage instructions at https://pre-commit.com
+
+- id: yamllint
+ name: yamllint
+ description: This hook runs yamllint.
+ entry: yamllint
+ language: python
+ types: [file, yaml]
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..0b847d2
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,286 @@
+Changelog
+=========
+
+1.33.0 (2023-11-09)
+-------------------
+
+- Add support for Python 3.12, drop support for Python 3.7
+- Rule ``document-end``: fix spurious "missing document end"
+- Rule ``empty-values``: add ``forbid-in-block-sequences`` option
+
+1.32.0 (2023-05-22)
+-------------------
+
+- Look for configuration file in parent directories
+- Rule ``anchors``: add new option ``forbid-unused-anchors``
+
+1.31.0 (2023-04-21)
+-------------------
+
+- Build: migrate from ``setup.py`` to ``pyproject.toml``
+- Docs: update some outdated URLs
+- Rule ``colons``: prevent error when space before is mandatory
+
+1.30.0 (2023-03-22)
+-------------------
+
+- Rule ``anchors``: add new rule to detect undeclared or duplicated anchors
+- Python API: prevent using ``is_file_ignored()`` with null ``filepath``
+- Docs: fix misleading Python API example
+- Docs: fix plain text code snippet example
+- Docs: update pre-commit hook example
+
+1.29.0 (2023-01-10)
+-------------------
+
+- Add support for Python 3.11, drop support for Python 3.6
+- Rule ``float-values``: fix bug on strings containing fordidden values
+- Stop releasing universal wheels
+- Use proper Python 3 I/O type for file reading
+- Rule ``indentation``: fix ``indent-sequences`` in nested collections
+- Docs: clarify ``disable-line`` and parser errors, give a workaround
+- Refactors to apply some pyupgrade suggestions
+- Allow using a list of strings in ``ignore`` configuration
+- Add ``--list-files`` command line option
+
+1.28.0 (2022-09-12)
+-------------------
+
+- Better compress PNG image in documentation
+- Remove ``__future__`` imports specific to Python 2
+- Remove inheritance from ``object`` specific to Python 2
+- Simplify GitHub Actions example in documentation
+- Update ALE vim plugin link in documentation
+- Update license to latest version of GPLv3
+- Pre-compile disable/enable rules regexes
+- Rule ``quoted-strings``: add ``allow-quoted-quotes`` option
+- Add option ``ignore-from-file`` in config
+
+1.27.1 (2022-07-08)
+-------------------
+
+- Fix failing test on ``key-duplicates`` for old PyYAML versions
+
+1.27.0 (2022-07-08)
+-------------------
+
+- Add support for Python 3.10, drop Python 3.5
+- Fix GitHub Actions workflow
+- Refactor ``--format=auto`` logic
+- Update GitHub format output to use groups
+- Rule ``comments``: allow whitespace after the shebang marker
+- Multiple minor fixes in documentation
+- Configure Sphinx to make man page show up in apropos
+- Attempt to clarify configuration file location in documentation
+- Rule ``key-duplicates``: don't crash on redundant closing brackets or braces
+- Use ``rstcheck`` to lint documentation on the CI
+- Remove UTF-8 headers in Python files, since Python 2 isn't supported
+- Add various tests to increase coverage
+- Rule ``octal-values``: pre-compile regex for performance
+- Add sections for Visual Studio Code and IntelliJ in documentation
+- Rule ``new-lines``: add the ``type: platform`` config option
+- Add the new rule ``float-values``
+
+1.26.3 (2021-08-21)
+-------------------
+
+- Restore runtime dependency ``setuptools`` for Python < 3.8
+
+1.26.2 (2021-08-03)
+-------------------
+
+- Fix ``python_requires`` to comply with PEP 345 and PEP 440
+
+1.26.1 (2021-04-06)
+-------------------
+
+- Remove runtime dependency ``setuptools`` for Python < 3.8
+- Fix ``line_length`` to skip all hash signs starting comment
+
+1.26.0 (2021-01-29)
+-------------------
+
+- End support for Python 2 and Python 3.4, add support for Python 3.9
+- Add ``forbid: non-empty`` option to ``braces`` and ``brackets`` rules
+- Fix ``quoted-strings`` for explicit octal recognition
+- Add documentation for integration with Arcanist
+- Fix typos in changelog and README
+- Stop using deprecated ``python setup.py test`` in tests
+
+1.25.0 (2020-09-29)
+-------------------
+
+- Run tests on Travis both with and without UTF-8 locales
+- Improve documentation with default values to rules with options
+- Improve documentation with a Python API usage example
+- Fix documentation on ``commas`` examples
+- Packaging: move setuptools' configuration from ``setup.py`` to ``setup.cfg``
+- Packaging: add extra info in PyPI metadata
+- Improve documentation on ``yaml-files``
+- Fix ``octal-values`` to prevent detection of ``8`` and ``9`` as octal values
+- Fix ``quoted-strings`` Fix detecting strings with hashtag as requiring quotes
+- Add ``forbid`` configuration to the ``braces`` and ``brackets`` rules
+- Fix runtime dependencies missing ``setuptools``
+- Add a new output format for GitHub Annotations (``--format github``)
+- Fix DOS lines messing with rule IDs in directives
+
+1.24.2 (2020-07-16)
+-------------------
+
+- Add ``locale`` config option and make ``key-ordering`` locale-aware
+
+1.24.1 (2020-07-15)
+-------------------
+
+- Revert ``locale`` config option from version 1.24.0 because of a bug
+
+1.24.0 (2020-07-15)
+-------------------
+
+- Specify config with environment variable ``YAMLLINT_CONFIG_FILE``
+- Fix bug with CRLF in ``new-lines`` and ``require-starting-space``
+- Do not run linter on directories whose names look like YAML files
+- Add ``locale`` config option and make ``key-ordering`` locale-aware
+
+1.23.0 (2020-04-17)
+-------------------
+
+- Allow rules to validate their configuration
+- Add options ``extra-required`` and ``extra-allowed`` to ``quoted-strings``
+
+1.22.1 (2020-04-15)
+-------------------
+
+- Fix ``quoted-strings`` rule with ``only-when-needed`` on corner cases
+
+1.22.0 (2020-04-13)
+-------------------
+
+- Add ``check-keys`` option to the ``truthy`` rule
+- Fix ``quoted-strings`` rule not working on sequences items
+- Sunset Python 2
+
+1.21.0 (2020-03-24)
+-------------------
+
+- Fix ``new-lines`` rule on Python 3 with DOS line endings
+- Fix ``quoted-strings`` rule not working for string values matching scalars
+- Add ``required: only-when-needed`` option to the ``quoted-strings`` rule
+
+1.20.0 (2019-12-26)
+-------------------
+
+- Add --no-warnings option to suppress warning messages
+- Use 'syntax' as rule name upon syntax errors
+
+1.19.0 (2019-11-19)
+-------------------
+
+- Allow disabling all checks for a file with ``# yamllint disable-file``
+
+1.18.0 (2019-10-15)
+-------------------
+
+- Lint ``.yamllint`` config file by default
+- Also read config from ``.yamllint.yml`` and ``.yamllint.yaml``
+- Improve documentation for ``yaml-files``
+- Update documentation for ``pre-commit``
+- Explicitly disable ``empty-values`` and ``octal-values`` rules
+
+1.17.0 (2019-08-12)
+-------------------
+
+- Simplify installation instructions in the README
+- Add OpenBSD installation instructions
+- Make YAML file extensions configurable
+
+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)
+-------------------
+
+- Allow linting from standard input with ``yamllint -``
+
+1.14.0 (2019-01-14)
+-------------------
+
+- Fix documentation code snippets
+- Drop Python 2.6 and 3.3 support, add Python 3.7 support
+- Update documentation and tests for ``line-length`` + Unicode + Python 2
+- Allow rule configurations to lack options
+- Add a new ``ignore-shebangs`` option for the ``comments`` rule
+
+1.13.0 (2018-11-14)
+-------------------
+
+- Use ``isinstance(x, y)`` instead of ``type(x) == y``
+- Add a new ``-f colored`` option
+- Update documentation about colored output when run from CLI
+
+1.12.1 (2018-10-17)
+-------------------
+
+- Fix the ``quoted-strings`` rule, broken implementation
+- Fix missing documentation for the ``quoted-strings`` rule
+
+1.12.0 (2018-10-04)
+-------------------
+
+- Add a new ``quoted-strings`` rule
+- Update installation documentation for pip, CentOS, Debian, Ubuntu, Mac OS
+
+1.11.1 (2018-04-06)
+-------------------
+
+- Handle merge keys (``<<``) in the ``key-duplicates`` rule
+- Update documentation about pre-commit
+- Make examples for ``ignore`` rule clearer
+- Clarify documentation on the 'truthy' rule
+- Fix crash in parser due to a change in PyYAML > 3.12
+
+1.11.0 (2018-02-21)
+-------------------
+
+- Add a new ``octal-values`` rule
+
+1.10.0 (2017-11-05)
+-------------------
+
+- Fix colored output on Windows
+- Check documentation compilation on continuous integration
+- Add a new ``empty-values`` rule
+- Make sure test files are included in dist bundle
+- Tests: Use en_US.UTF-8 locale when C.UTF-8 not available
+- Tests: Dynamically detect Python executable path
+
+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)
+------------------
+
+- Require pathspec >= 0.5.3
+- Support Python 2.6
+- Add a changelog
+
+1.8.0 (2017-06-28)
+------------------
+
+- Refactor argparse with mutually_exclusive_group
+- Add support to ignore paths in configuration
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..312c6d8
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,48 @@
+Contributing
+============
+
+Pull requests are the best way to propose changes to the codebase.
+Contributions are welcome, but they have to meet some criteria.
+
+Pull Request Process
+--------------------
+
+1. Fork this Git repository and create your branch from ``master``.
+
+2. Make sure the tests pass:
+
+ .. code:: bash
+
+ pip install --user .
+ python -m unittest discover # all tests...
+ python -m unittest tests/rules/test_commas.py # or just some tests (faster)
+
+3. If you add code that should be tested, add tests.
+
+4. Make sure the linters pass:
+
+ .. code:: bash
+
+ flake8 .
+
+ If you added/modified documentation:
+
+ .. code:: bash
+
+ doc8 $(git ls-files '*.rst')
+
+ If you touched YAML files:
+
+ .. code:: bash
+
+ yamllint --strict $(git ls-files '*.yaml' '*.yml')
+
+5. If relevant, update documentation (either in ``docs`` directly or in rules
+ files themselves).
+
+6. Write a `good commit message
+ <http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html>`_.
+ If the pull request has multiple commits, each must be atomic (single
+ irreducible change that makes sense on its own).
+
+7. Then, open a pull request.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..3f2ab00
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,140 @@
+yamllint
+========
+
+A linter for YAML files.
+
+yamllint does not only check for syntax validity, but for weirdnesses like key
+repetition and cosmetic problems such as lines length, trailing spaces,
+indentation, etc.
+
+.. image::
+ https://github.com/adrienverge/yamllint/actions/workflows/ci.yaml/badge.svg?branch=master
+ :target: https://github.com/adrienverge/yamllint/actions/workflows/ci.yaml?query=branch%3Amaster
+ :alt: CI tests status
+.. image::
+ https://coveralls.io/repos/github/adrienverge/yamllint/badge.svg?branch=master
+ :target: https://coveralls.io/github/adrienverge/yamllint?branch=master
+ :alt: Code coverage status
+.. image:: https://readthedocs.org/projects/yamllint/badge/?version=latest
+ :target: https://yamllint.readthedocs.io/en/latest/?badge=latest
+ :alt: Documentation status
+
+Written in Python (compatible with Python 3 only).
+
+Documentation
+-------------
+
+https://yamllint.readthedocs.io/
+
+Overview
+--------
+
+Screenshot
+^^^^^^^^^^
+
+.. image:: docs/screenshot.png
+ :alt: yamllint screenshot
+
+Installation
+^^^^^^^^^^^^
+
+Using pip, the Python package manager:
+
+.. code:: bash
+
+ pip install --user yamllint
+
+yamllint is also packaged for all major operating systems, see installation
+examples (``dnf``, ``apt-get``...) `in the documentation
+<https://yamllint.readthedocs.io/en/stable/quickstart.html>`_.
+
+Usage
+^^^^^
+
+.. code:: bash
+
+ # Lint one or more files
+ yamllint my_file.yml my_other_file.yaml ...
+
+.. code:: bash
+
+ # Lint all YAML files in a directory
+ yamllint .
+
+.. code:: bash
+
+ # Use a pre-defined lint configuration
+ yamllint -d relaxed file.yaml
+
+ # Use a custom lint configuration
+ yamllint -c /path/to/myconfig file-to-lint.yaml
+
+.. code:: bash
+
+ # Output a parsable format (for syntax checking in editors like Vim, emacs...)
+ yamllint -f parsable file.yaml
+
+`Read more in the complete documentation! <https://yamllint.readthedocs.io/>`__
+
+Features
+^^^^^^^^
+
+Here is a yamllint configuration file example:
+
+.. code:: yaml
+
+ extends: default
+
+ rules:
+ # 80 chars should be enough, but don't fail if a line is longer
+ line-length:
+ max: 80
+ level: warning
+
+ # don't bother me with this rule
+ indentation: disable
+
+Within a YAML file, special comments can be used to disable checks for a single
+line:
+
+.. code:: yaml
+
+ This line is waaaaaaaaaay too long # yamllint disable-line
+
+or for a whole block:
+
+.. code:: yaml
+
+ # yamllint disable rule:colons
+ - Lorem : ipsum
+ dolor : sit amet,
+ consectetur : adipiscing elit
+ # yamllint enable
+
+Specific files can be ignored (totally or for some rules only) using a
+``.gitignore``-style pattern:
+
+.. code:: yaml
+
+ # For all rules
+ ignore: |
+ *.dont-lint-me.yaml
+ /bin/
+ !/bin/*.lint-me-anyway.yaml
+
+ rules:
+ key-duplicates:
+ ignore: |
+ generated
+ *.template.yaml
+ trailing-spaces:
+ ignore: |
+ *.ignore-trailing-spaces.yaml
+ /ascii-art/*
+
+`Read more in the complete documentation! <https://yamllint.readthedocs.io/>`__
+
+License
+-------
+
+`GPL version 3 <LICENSE>`_
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..0426d42
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS = -W
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/yamllint.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/yamllint.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/yamllint"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/yamllint"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..2c40177
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,54 @@
+# yamllint documentation build configuration file, created by
+# sphinx-quickstart on Thu Jan 21 21:18:52 2016.
+
+import sys
+import os
+from unittest.mock import MagicMock
+
+sys.path.insert(0, os.path.abspath('..'))
+
+from yamllint import __copyright__, APP_NAME, APP_VERSION # noqa
+
+# -- General configuration ------------------------------------------------
+
+extensions = [
+ 'sphinx.ext.autodoc',
+]
+
+source_suffix = '.rst'
+
+master_doc = 'index'
+
+project = APP_NAME
+copyright = __copyright__.lstrip('Copyright ')
+
+version = APP_VERSION
+release = APP_VERSION
+
+pygments_style = 'sphinx'
+
+# -- Options for HTML output ----------------------------------------------
+
+html_theme = 'default'
+
+htmlhelp_basename = 'yamllintdoc'
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'yamllint', 'Linter for YAML files', ['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/configuration.rst b/docs/configuration.rst
new file mode 100644
index 0000000..9624b49
--- /dev/null
+++ b/docs/configuration.rst
@@ -0,0 +1,255 @@
+Configuration
+=============
+
+yamllint uses a set of :doc:`rules <rules>` to check source files for problems.
+Each rule is independent from the others, and can be enabled, disabled or
+tweaked. All these settings can be gathered in a configuration file.
+
+To use a custom configuration file, use the ``-c`` option:
+
+.. code:: bash
+
+ yamllint -c /path/to/myconfig file-to-lint.yaml
+
+If ``-c`` is not provided, yamllint will look for a configuration file in the
+following locations (by order of preference):
+
+- a file named ``.yamllint``, ``.yamllint.yaml``, or ``.yamllint.yml`` in the
+ current working directory, or a parent directory (the search for this file is
+ terminated at the user's home or filesystem root)
+- a filename referenced by ``$YAMLLINT_CONFIG_FILE``, if set
+- a file named ``$XDG_CONFIG_HOME/yamllint/config`` or
+ ``~/.config/yamllint/config``, if present
+
+Finally if no config file is found, the default configuration is applied.
+
+Default configuration
+---------------------
+
+Unless told otherwise, yamllint uses its ``default`` configuration:
+
+.. literalinclude:: ../yamllint/conf/default.yaml
+ :language: yaml
+
+Details on rules can be found on :doc:`the rules page <rules>`.
+
+There is another pre-defined configuration named ``relaxed``. As its name
+suggests, it is more tolerant:
+
+.. literalinclude:: ../yamllint/conf/relaxed.yaml
+ :language: yaml
+
+It can be chosen using:
+
+.. code:: bash
+
+ yamllint -d relaxed file.yml
+
+Extending the default configuration
+-----------------------------------
+
+When writing a custom configuration file, you don't need to redefine every
+rule. Just extend the ``default`` configuration (or any already-existing
+configuration file).
+
+For instance, if you just want to disable the ``comments-indentation`` rule,
+your file could look like this:
+
+.. code-block:: yaml
+
+ # This is my first, very own configuration file for yamllint!
+ # It extends the default conf by adjusting some options.
+
+ extends: default
+
+ rules:
+ comments-indentation: disable # don't bother me with this rule
+
+Similarly, if you want to set the ``line-length`` rule as a warning and be less
+strict on block sequences indentation:
+
+.. code-block:: yaml
+
+ extends: default
+
+ rules:
+ # 80 chars should be enough, but don't fail if a line is longer
+ line-length:
+ max: 80
+ level: warning
+
+ # accept both key:
+ # - item
+ #
+ # and key:
+ # - item
+ indentation:
+ indent-sequences: whatever
+
+Custom configuration without a config file
+------------------------------------------
+
+It is possible -- although not recommended -- to pass custom configuration
+options to yamllint with the ``-d`` (short for ``--config-data``) option.
+
+Its content can either be the name of a pre-defined conf (example: ``default``
+or ``relaxed``) or a serialized YAML object describing the configuration.
+
+For instance:
+
+.. code:: bash
+
+ yamllint -d "{extends: relaxed, rules: {line-length: {max: 120}}}" file.yaml
+
+Errors and warnings
+-------------------
+
+Problems detected by yamllint can be raised either as errors or as warnings.
+The CLI will output them (with different colors when using the ``colored``
+output format, or ``auto`` when run from a terminal).
+
+By default the script will exit with a return code ``1`` *only when* there is
+one or more error(s).
+
+However if strict mode is enabled with the ``-s`` (or ``--strict``) option, the
+return code will be:
+
+ * ``0`` if no errors or warnings occur
+ * ``1`` if one or more errors occur
+ * ``2`` if no errors occur, but one or more warnings occur
+
+If the script is invoked with the ``--no-warnings`` option, it won't output
+warning level problems, only error level ones.
+
+YAML files extensions
+---------------------
+
+To configure what yamllint should consider as YAML files when listing
+directories, set ``yaml-files`` configuration option. The default is:
+
+.. code-block:: yaml
+
+ yaml-files:
+ - '*.yaml'
+ - '*.yml'
+ - '.yamllint'
+
+The same rules as for ignoring paths apply (``.gitignore``-style path pattern,
+see below).
+
+If you need to know the exact list of files that yamllint would process,
+without really linting them, you can use ``--list-files``:
+
+.. code:: bash
+
+ yamllint --list-files .
+
+Ignoring paths
+--------------
+
+It is possible to exclude specific files or directories, so that the linter
+doesn't process them. They can be provided either as a list of paths, or as a
+bulk string.
+
+You can either totally ignore files (they won't be looked at):
+
+.. code-block:: yaml
+
+ extends: default
+
+ ignore: |
+ /this/specific/file.yaml
+ all/this/directory/
+ *.template.yaml
+
+ # or:
+
+ ignore:
+ - /this/specific/file.yaml
+ - all/this/directory/
+ - '*.template.yaml'
+
+or ignore paths only for specific rules:
+
+.. code-block:: yaml
+
+ extends: default
+
+ rules:
+ trailing-spaces:
+ ignore: |
+ /this-file-has-trailing-spaces-but-it-is-OK.yaml
+ /generated/*.yaml
+
+ # or:
+
+ rules:
+ trailing-spaces:
+ ignore:
+ - /this-file-has-trailing-spaces-but-it-is-OK.yaml
+ - /generated/*.yaml
+
+Note that this ``.gitignore``-style path pattern allows complex path
+exclusion/inclusion, see the `pathspec README file
+<https://pypi.org/project/pathspec/>`_ for more details. Here is a more complex
+example:
+
+.. code-block:: yaml
+
+ # For all rules
+ ignore: |
+ *.dont-lint-me.yaml
+ /bin/
+ !/bin/*.lint-me-anyway.yaml
+
+ extends: default
+
+ rules:
+ key-duplicates:
+ ignore: |
+ generated
+ *.template.yaml
+ trailing-spaces:
+ ignore: |
+ *.ignore-trailing-spaces.yaml
+ ascii-art/*
+
+You can also use the ``.gitignore`` file (or any list of files) through:
+
+.. code-block:: yaml
+
+ ignore-from-file: .gitignore
+
+or:
+
+.. code-block:: yaml
+
+ ignore-from-file: [.gitignore, .yamlignore]
+
+.. note:: However, this is mutually exclusive with the ``ignore`` key.
+
+If you need to know the exact list of files that yamllint would process,
+without really linting them, you can use ``--list-files``:
+
+.. code:: bash
+
+ yamllint --list-files .
+
+Setting the locale
+------------------
+
+It is possible to set the ``locale`` option globally. This is passed to Python's
+`locale.setlocale
+<https://docs.python.org/3/library/locale.html#locale.setlocale>`_,
+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.
+
+.. code-block:: yaml
+
+ extends: default
+
+ locale: en_US.UTF-8
diff --git a/docs/development.rst b/docs/development.rst
new file mode 100644
index 0000000..b179de2
--- /dev/null
+++ b/docs/development.rst
@@ -0,0 +1,18 @@
+Development
+===========
+
+yamllint provides both a script and a Python module. The latter can be used to
+write your own linting tools.
+
+Basic example of running the linter from Python:
+
+.. code-block:: python
+
+ import yamllint
+
+ yaml_config = yamllint.config.YamlLintConfig("extends: default")
+ for p in yamllint.linter.run(open("example.yaml", "r"), yaml_config):
+ print(p.desc, p.line, p.rule)
+
+.. automodule:: yamllint.linter
+ :members:
diff --git a/docs/disable_with_comments.rst b/docs/disable_with_comments.rst
new file mode 100644
index 0000000..a973da6
--- /dev/null
+++ b/docs/disable_with_comments.rst
@@ -0,0 +1,136 @@
+Disable with comments
+=====================
+
+Disabling checks for a specific line
+------------------------------------
+
+To prevent yamllint from reporting problems for a specific line, add a
+directive comment (``# yamllint disable-line ...``) on that line, or on the
+line above. For instance:
+
+.. code-block:: yaml
+
+ # The following mapping contains the same key twice,
+ # but I know what I'm doing:
+ key: value 1
+ key: value 2 # yamllint disable-line rule:key-duplicates
+
+ - This line is waaaaaaaaaay too long but yamllint will not report anything about it. # yamllint disable-line rule:line-length
+ This line will be checked by yamllint.
+
+or:
+
+.. code-block:: yaml
+
+ # The following mapping contains the same key twice,
+ # but I know what I'm doing:
+ key: value 1
+ # yamllint disable-line rule:key-duplicates
+ key: value 2
+
+ # yamllint disable-line rule:line-length
+ - This line is waaaaaaaaaay too long but yamllint will not report anything about it.
+ This line will be checked by yamllint.
+
+It is possible, although not recommend, to disabled **all** rules for a
+specific line:
+
+.. code-block:: yaml
+
+ # yamllint disable-line
+ - { all : rules ,are disabled for this line}
+
+You can't make yamllint ignore invalid YAML syntax on a line (which generates a
+`syntax error`), such as when templating a YAML file with Jinja. In some cases,
+you can workaround this by putting the templating syntax in a YAML comment. See
+`Putting template flow control in comments`_.
+
+If you need to disable multiple rules, it is allowed to chain rules like this:
+``# yamllint disable-line rule:hyphens rule:commas rule:indentation``.
+
+Disabling checks for all (or part of) the file
+----------------------------------------------
+
+To prevent yamllint from reporting problems for the whole file, or for a block
+of lines within the file, use ``# yamllint disable ...`` and ``# yamllint
+enable ...`` directive comments. For instance:
+
+.. code-block:: yaml
+
+ # yamllint disable rule:colons
+ - Lorem : ipsum
+ dolor : sit amet,
+ consectetur : adipiscing elit
+ # yamllint enable rule:colons
+
+ - rest of the document...
+
+It is possible, although not recommend, to disabled **all** rules:
+
+.. code-block:: yaml
+
+ # yamllint disable
+ - Lorem :
+ ipsum:
+ dolor : [ sit,amet]
+ - consectetur : adipiscing elit
+ # yamllint enable
+
+If you need to disable multiple rules, it is allowed to chain rules like this:
+``# yamllint disable rule:hyphens rule:commas rule:indentation``.
+
+Disabling all checks for a file
+-------------------------------
+
+To prevent yamllint from reporting problems for a specific file, add the
+directive comment ``# yamllint disable-file`` as the first line of the file.
+For instance:
+
+.. code-block:: yaml
+
+ # yamllint disable-file
+ # The following mapping contains the same key twice, but I know what I'm doing:
+ key: value 1
+ key: value 2
+
+ - This line is waaaaaaaaaay too long but yamllint will not report anything about it.
+
+or:
+
+.. code-block:: jinja
+
+ # yamllint disable-file
+ # This file is not valid YAML because it is a Jinja template
+ {% if extra_info %}
+ key1: value1
+ {% endif %}
+ key2: value2
+
+Putting template flow control in comments
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Alternatively for templating you can wrap the template statements in comments
+to make it a valid YAML file. As long as the templating language doesn't use
+the same comment symbol, it should be a valid template and valid YAML (pre and
+post-template processing).
+
+Example of a Jinja2 code that cannot be parsed as YAML because it contains
+invalid tokens ``{%`` and ``%}``:
+
+.. code-block:: text
+
+ # This file IS NOT valid YAML and will produce syntax errors
+ {% if extra_info %}
+ key1: value1
+ {% endif %}
+ key2: value2
+
+But it can be fixed using YAML comments:
+
+.. code-block:: yaml
+
+ # This file IS valid YAML because the Jinja is in a YAML comment
+ # {% if extra_info %}
+ key1: value1
+ # {% endif %}
+ key2: value2
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..5456d6a
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,29 @@
+yamllint documentation
+======================
+
+.. automodule:: yamllint
+
+Screenshot
+----------
+
+.. image:: screenshot.png
+ :alt: yamllint screenshot
+
+.. note::
+
+ The default output format is inspired by `eslint <https://eslint.org/>`_, a
+ great linting tool for Javascript.
+
+Table of contents
+-----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ quickstart
+ configuration
+ rules
+ disable_with_comments
+ development
+ text_editors
+ integration
diff --git a/docs/integration.rst b/docs/integration.rst
new file mode 100644
index 0000000..9a6a935
--- /dev/null
+++ b/docs/integration.rst
@@ -0,0 +1,67 @@
+Integration with other software
+===============================
+
+Integration with pre-commit
+---------------------------
+
+You can integrate yamllint in the `pre-commit <https://pre-commit.com/>`_ tool.
+Here is an example, to add in your .pre-commit-config.yaml
+
+.. code:: yaml
+
+ ---
+ # Update the rev variable with the release version that you want, from the yamllint repo
+ # You can pass your custom .yamllint with args attribute.
+ repos:
+ - repo: https://github.com/adrienverge/yamllint.git
+ rev: v1.29.0
+ hooks:
+ - id: yamllint
+ args: [--strict, -c=/path/to/.yamllint]
+
+
+Integration with GitHub Actions
+-------------------------------
+
+yamllint auto-detects when it's running inside of `GitHub
+Actions <https://github.com/features/actions>`_ and automatically uses the
+suited output format to decorate code with linting errors. You can also force
+the GitHub Actions output with ``yamllint --format github``.
+
+A minimal example workflow using GitHub Actions:
+
+.. code:: yaml
+
+ ---
+ on: push # yamllint disable-line rule:truthy
+
+ jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install yamllint
+ run: pip install yamllint
+
+ - name: Lint YAML files
+ run: yamllint .
+
+Integration with Arcanist
+-------------------------
+
+You can configure yamllint to run on ``arc lint``. Here is an example
+``.arclint`` file that makes use of this configuration.
+
+.. code:: json
+
+ {
+ "linters": {
+ "yamllint": {
+ "type": "script-and-regex",
+ "script-and-regex.script": "yamllint",
+ "script-and-regex.regex": "/^(?P<line>\\d+):(?P<offset>\\d+) +(?P<severity>warning|error) +(?P<message>.*) +\\((?P<name>.*)\\)$/m",
+ "include": "(\\.(yml|yaml)$)"
+ }
+ }
+ }
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
new file mode 100644
index 0000000..9828dd6
--- /dev/null
+++ b/docs/quickstart.rst
@@ -0,0 +1,115 @@
+Quickstart
+==========
+
+Installing yamllint
+-------------------
+
+On Fedora / CentOS (note: `EPEL <https://docs.fedoraproject.org/en-US/epel/>`_ is
+required on CentOS):
+
+.. code:: bash
+
+ sudo dnf install yamllint
+
+On Debian 8+ / Ubuntu 16.04+:
+
+.. code:: bash
+
+ sudo apt-get install yamllint
+
+On Mac OS 10.11+:
+
+.. code:: bash
+
+ brew install yamllint
+
+On FreeBSD:
+
+.. code:: sh
+
+ pkg install py36-yamllint
+
+On OpenBSD:
+
+.. code:: sh
+
+ doas pkg_add py3-yamllint
+
+Alternatively using pip, the Python package manager:
+
+.. code:: bash
+
+ pip install --user yamllint
+
+If you prefer installing from source, you can run, from the source directory:
+
+.. code:: bash
+
+ python -m build
+ pip install --user dist/yamllint-*.tar.gz
+
+Running yamllint
+----------------
+
+Basic usage:
+
+.. code:: bash
+
+ yamllint file.yml other-file.yaml
+
+You can also lint all YAML files in a whole directory:
+
+.. code:: bash
+
+ yamllint .
+
+Or lint a YAML stream from standard input:
+
+.. code:: bash
+
+ echo -e 'this: is\nvalid: YAML' | yamllint -
+
+The output will look like (colors are not displayed here):
+
+::
+
+ file.yml
+ 1:4 error trailing spaces (trailing-spaces)
+ 4:4 error wrong indentation: expected 4 but found 3 (indentation)
+ 5:4 error duplication of key "id-00042" in mapping (key-duplicates)
+ 6:6 warning comment not indented like content (comments-indentation)
+ 12:6 error too many spaces after hyphen (hyphens)
+ 15:12 error too many spaces before comma (commas)
+
+ other-file.yaml
+ 1:1 warning missing document start "---" (document-start)
+ 6:81 error line too long (87 > 80 characters) (line-length)
+ 10:1 error too many blank lines (4 > 2) (empty-lines)
+ 11:4 error too many spaces inside braces (braces)
+
+By default, the output of yamllint is colored when run from a terminal, and
+pure text in other cases. Add the ``-f standard`` arguments to force
+non-colored output. Use the ``-f colored`` arguments to force colored output.
+
+Add the ``-f parsable`` arguments if you need an output format parsable by a
+machine (for instance for :doc:`syntax highlighting in text editors
+<text_editors>`). The output will then look like:
+
+::
+
+ file.yml:6:2: [warning] missing starting space in comment (comments)
+ file.yml:57:1: [error] trailing spaces (trailing-spaces)
+ file.yml:60:3: [error] wrong indentation: expected 4 but found 2 (indentation)
+
+If you have a custom linting configuration file (see :doc:`how to configure
+yamllint <configuration>`), it can be passed to yamllint using the ``-c``
+option:
+
+.. code:: bash
+
+ yamllint -c ~/myconfig file.yaml
+
+.. note::
+
+ If you have a ``.yamllint`` file in your working directory, it will be
+ automatically loaded as configuration by yamllint.
diff --git a/docs/rules.rst b/docs/rules.rst
new file mode 100644
index 0000000..eb3bc82
--- /dev/null
+++ b/docs/rules.rst
@@ -0,0 +1,131 @@
+Rules
+=====
+
+When linting a document with yamllint, a series of rules (such as
+``line-length``, ``trailing-spaces``, etc.) are checked against.
+
+A :doc:`configuration file <configuration>` can be used to enable or disable
+these rules, to set their level (*error* or *warning*), but also to tweak their
+options.
+
+This page describes the rules and their options.
+
+.. contents:: List of rules
+ :local:
+ :depth: 1
+
+anchors
+-------
+
+.. automodule:: yamllint.rules.anchors
+
+braces
+------
+
+.. automodule:: yamllint.rules.braces
+
+brackets
+--------
+
+.. automodule:: yamllint.rules.brackets
+
+colons
+------
+
+.. automodule:: yamllint.rules.colons
+
+commas
+------
+
+.. automodule:: yamllint.rules.commas
+
+comments
+--------
+
+.. automodule:: yamllint.rules.comments
+
+comments-indentation
+--------------------
+
+.. automodule:: yamllint.rules.comments_indentation
+
+document-end
+------------
+
+.. automodule:: yamllint.rules.document_end
+
+document-start
+--------------
+
+.. automodule:: yamllint.rules.document_start
+
+empty-lines
+-----------
+
+.. automodule:: yamllint.rules.empty_lines
+
+empty-values
+------------
+
+.. automodule:: yamllint.rules.empty_values
+
+float-values
+------------
+
+.. automodule:: yamllint.rules.float_values
+
+
+hyphens
+-------
+
+.. automodule:: yamllint.rules.hyphens
+
+indentation
+-----------
+
+.. automodule:: yamllint.rules.indentation
+
+key-duplicates
+--------------
+
+.. automodule:: yamllint.rules.key_duplicates
+
+key-ordering
+--------------
+
+.. automodule:: yamllint.rules.key_ordering
+
+line-length
+-----------
+
+.. automodule:: yamllint.rules.line_length
+
+new-line-at-end-of-file
+-----------------------
+
+.. automodule:: yamllint.rules.new_line_at_end_of_file
+
+new-lines
+---------
+
+.. automodule:: yamllint.rules.new_lines
+
+octal-values
+------------
+
+.. automodule:: yamllint.rules.octal_values
+
+quoted-strings
+--------------
+
+.. automodule:: yamllint.rules.quoted_strings
+
+trailing-spaces
+---------------
+
+.. automodule:: yamllint.rules.trailing_spaces
+
+truthy
+---------------
+
+.. automodule:: yamllint.rules.truthy
diff --git a/docs/screenshot.png b/docs/screenshot.png
new file mode 100644
index 0000000..6bad6c0
--- /dev/null
+++ b/docs/screenshot.png
Binary files differ
diff --git a/docs/text_editors.rst b/docs/text_editors.rst
new file mode 100644
index 0000000..12387e6
--- /dev/null
+++ b/docs/text_editors.rst
@@ -0,0 +1,52 @@
+Integration with text editors
+=============================
+
+Most text editors support syntax checking and highlighting, to visually report
+syntax errors and warnings to the user. yamllint can be used to syntax-check
+YAML source, but a bit of configuration is required depending on your favorite
+text editor.
+
+Vim
+---
+
+Assuming that the `ALE <https://github.com/dense-analysis/ale>`_ plugin is
+installed, yamllint is supported by default. It is automatically enabled when
+editing YAML files.
+
+If you instead use the `syntastic <https://github.com/vim-syntastic/syntastic>`_
+plugin, add this to your ``.vimrc``:
+
+::
+
+ let g:syntastic_yaml_checkers = ['yamllint']
+
+Neovim
+------
+
+Assuming that the `neomake <https://github.com/neomake/neomake>`_ plugin is
+installed, yamllint is supported by default. It is automatically enabled when
+editing YAML files.
+
+Emacs
+-----
+
+If you are `flycheck <https://github.com/flycheck/flycheck>`_ user, you can use
+`flycheck-yamllint <https://github.com/krzysztof-magosa/flycheck-yamllint>`_ integration.
+
+Visual Studio Code
+------------------
+
+https://marketplace.visualstudio.com/items?itemName=fnando.linter
+
+IntelliJ
+--------
+
+https://plugins.jetbrains.com/plugin/15349-yamllint
+
+Other text editors
+------------------
+
+.. rubric:: Help wanted!
+
+Your favorite text editor is not listed here? Help us improve by adding a
+section (by opening a pull-request or issue on GitHub).
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..63746f6
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,54 @@
+[project]
+name = "yamllint"
+description = "A linter for YAML files."
+readme = {file = "README.rst", content-type = "text/x-rst"}
+requires-python = ">=3.8"
+license = {text = "GPL-3.0-or-later"}
+authors = [{name = "Adrien Vergé"}]
+keywords = ["yaml", "lint", "linter", "syntax", "checker"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+ "Programming Language :: Python",
+ "Topic :: Software Development",
+ "Topic :: Software Development :: Debuggers",
+ "Topic :: Software Development :: Quality Assurance",
+ "Topic :: Software Development :: Testing",
+]
+dependencies = [
+ "pathspec >= 0.5.3",
+ "pyyaml",
+]
+dynamic = ["version"]
+
+[project.optional-dependencies]
+dev = [
+ "doc8",
+ "flake8",
+ "flake8-import-order",
+ "rstcheck[sphinx]",
+ "sphinx",
+]
+
+[project.scripts]
+yamllint = "yamllint.cli:run"
+
+[project.urls]
+homepage = "https://github.com/adrienverge/yamllint"
+repository = "https://github.com/adrienverge/yamllint"
+documentation = "https://yamllint.readthedocs.io"
+
+[build-system]
+build-backend = "setuptools.build_meta"
+requires = ["setuptools >= 61"]
+
+[tool.setuptools]
+packages = ["yamllint", "yamllint.conf", "yamllint.rules"]
+
+[tool.setuptools.package-data]
+yamllint = ["conf/*.yaml"]
+
+[tool.setuptools.dynamic]
+version = {attr = "yamllint.__version__"}
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..ca78596
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,20 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from setuptools import setup
+
+# This is only kept for backward-compatibility with older versions that don't
+# support new packaging standards (e.g. PEP 517 or PEP 660):
+setup()
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..da1cd75
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,19 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import locale
+
+
+locale.setlocale(locale.LC_ALL, 'C')
diff --git a/tests/common.py b/tests/common.py
new file mode 100644
index 0000000..65af63b
--- /dev/null
+++ b/tests/common.py
@@ -0,0 +1,86 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import contextlib
+import os
+import shutil
+import tempfile
+import unittest
+
+import yaml
+
+from yamllint.config import YamlLintConfig
+from yamllint import linter
+
+
+class RuleTestCase(unittest.TestCase):
+ def build_fake_config(self, conf):
+ if conf is None:
+ conf = {}
+ else:
+ conf = yaml.safe_load(conf)
+ conf = {'extends': 'default',
+ 'rules': conf}
+ return YamlLintConfig(yaml.safe_dump(conf))
+
+ def check(self, source, conf, **kwargs):
+ expected_problems = []
+ for key in kwargs:
+ assert key.startswith('problem')
+ if len(kwargs[key]) > 2:
+ if kwargs[key][2] == 'syntax':
+ rule_id = None
+ else:
+ rule_id = kwargs[key][2]
+ else:
+ rule_id = self.rule_id
+ expected_problems.append(linter.LintProblem(
+ kwargs[key][0], kwargs[key][1], rule=rule_id))
+ expected_problems.sort()
+
+ real_problems = list(linter.run(source, self.build_fake_config(conf)))
+ self.assertEqual(real_problems, expected_problems)
+
+
+def build_temp_workspace(files):
+ tempdir = tempfile.mkdtemp(prefix='yamllint-tests-')
+
+ for path, content in files.items():
+ path = os.path.join(tempdir, path).encode('utf-8')
+ if not os.path.exists(os.path.dirname(path)):
+ os.makedirs(os.path.dirname(path))
+
+ if type(content) is list:
+ os.mkdir(path)
+ else:
+ mode = 'wb' if isinstance(content, bytes) else 'w'
+ with open(path, mode) as f:
+ f.write(content)
+
+ return tempdir
+
+
+@contextlib.contextmanager
+def temp_workspace(files):
+ """Provide a temporary workspace that is automatically cleaned up."""
+ backup_wd = os.getcwd()
+ wd = build_temp_workspace(files)
+
+ try:
+ os.chdir(wd)
+ yield
+ finally:
+ os.chdir(backup_wd)
+ shutil.rmtree(wd)
diff --git a/tests/rules/__init__.py b/tests/rules/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/rules/__init__.py
diff --git a/tests/rules/test_anchors.py b/tests/rules/test_anchors.py
new file mode 100644
index 0000000..7d7cbb7
--- /dev/null
+++ b/tests/rules/test_anchors.py
@@ -0,0 +1,281 @@
+# Copyright (C) 2023 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class AnchorsTestCase(RuleTestCase):
+ rule_id = 'anchors'
+
+ def test_disabled(self):
+ conf = 'anchors: disable'
+ self.check('---\n'
+ '- &b true\n'
+ '- &i 42\n'
+ '- &s hello\n'
+ '- &f_m {k: v}\n'
+ '- &f_s [1, 2]\n'
+ '- *b\n'
+ '- *i\n'
+ '- *s\n'
+ '- *f_m\n'
+ '- *f_s\n'
+ '---\n' # redeclare anchors in a new document
+ '- &b true\n'
+ '- &i 42\n'
+ '- &s hello\n'
+ '- *b\n'
+ '- *i\n'
+ '- *s\n'
+ '---\n'
+ 'block mapping: &b_m\n'
+ ' key: value\n'
+ 'extended:\n'
+ ' <<: *b_m\n'
+ ' foo: bar\n'
+ '---\n'
+ '{a: 1, &x b: 2, c: &y 3, *x : 4, e: *y}\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- &i 42\n'
+ '---\n'
+ '- &b true\n'
+ '- &b true\n'
+ '- &b true\n'
+ '- &s hello\n'
+ '- *b\n'
+ '- *i\n' # declared in a previous document
+ '- *f_m\n' # never declared
+ '- *f_m\n'
+ '- *f_m\n'
+ '- *f_s\n' # declared after
+ '- &f_s [1, 2]\n'
+ '---\n'
+ 'block mapping: &b_m\n'
+ ' key: value\n'
+ '---\n'
+ 'block mapping 1: &b_m_bis\n'
+ ' key: value\n'
+ 'block mapping 2: &b_m_bis\n'
+ ' key: value\n'
+ 'extended:\n'
+ ' <<: *b_m\n'
+ ' foo: bar\n'
+ '---\n'
+ '{a: 1, &x b: 2, c: &x 3, *x : 4, e: *y}\n'
+ '...\n', conf)
+
+ def test_forbid_undeclared_aliases(self):
+ conf = ('anchors:\n'
+ ' forbid-undeclared-aliases: true\n'
+ ' forbid-duplicated-anchors: false\n'
+ ' forbid-unused-anchors: false\n')
+ self.check('---\n'
+ '- &b true\n'
+ '- &i 42\n'
+ '- &s hello\n'
+ '- &f_m {k: v}\n'
+ '- &f_s [1, 2]\n'
+ '- *b\n'
+ '- *i\n'
+ '- *s\n'
+ '- *f_m\n'
+ '- *f_s\n'
+ '---\n' # redeclare anchors in a new document
+ '- &b true\n'
+ '- &i 42\n'
+ '- &s hello\n'
+ '- *b\n'
+ '- *i\n'
+ '- *s\n'
+ '---\n'
+ 'block mapping: &b_m\n'
+ ' key: value\n'
+ 'extended:\n'
+ ' <<: *b_m\n'
+ ' foo: bar\n'
+ '---\n'
+ '{a: 1, &x b: 2, c: &y 3, *x : 4, e: *y}\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- &i 42\n'
+ '---\n'
+ '- &b true\n'
+ '- &b true\n'
+ '- &b true\n'
+ '- &s hello\n'
+ '- *b\n'
+ '- *i\n' # declared in a previous document
+ '- *f_m\n' # never declared
+ '- *f_m\n'
+ '- *f_m\n'
+ '- *f_s\n' # declared after
+ '- &f_s [1, 2]\n'
+ '...\n'
+ '---\n'
+ 'block mapping: &b_m\n'
+ ' key: value\n'
+ '---\n'
+ 'block mapping 1: &b_m_bis\n'
+ ' key: value\n'
+ 'block mapping 2: &b_m_bis\n'
+ ' key: value\n'
+ 'extended:\n'
+ ' <<: *b_m\n'
+ ' foo: bar\n'
+ '---\n'
+ '{a: 1, &x b: 2, c: &x 3, *x : 4, e: *y}\n'
+ '...\n', conf,
+ problem1=(9, 3),
+ problem2=(10, 3),
+ problem3=(11, 3),
+ problem4=(12, 3),
+ problem5=(13, 3),
+ problem6=(25, 7),
+ problem7=(28, 37))
+
+ def test_forbid_duplicated_anchors(self):
+ conf = ('anchors:\n'
+ ' forbid-undeclared-aliases: false\n'
+ ' forbid-duplicated-anchors: true\n'
+ ' forbid-unused-anchors: false\n')
+ self.check('---\n'
+ '- &b true\n'
+ '- &i 42\n'
+ '- &s hello\n'
+ '- &f_m {k: v}\n'
+ '- &f_s [1, 2]\n'
+ '- *b\n'
+ '- *i\n'
+ '- *s\n'
+ '- *f_m\n'
+ '- *f_s\n'
+ '---\n' # redeclare anchors in a new document
+ '- &b true\n'
+ '- &i 42\n'
+ '- &s hello\n'
+ '- *b\n'
+ '- *i\n'
+ '- *s\n'
+ '---\n'
+ 'block mapping: &b_m\n'
+ ' key: value\n'
+ 'extended:\n'
+ ' <<: *b_m\n'
+ ' foo: bar\n'
+ '---\n'
+ '{a: 1, &x b: 2, c: &y 3, *x : 4, e: *y}\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- &i 42\n'
+ '---\n'
+ '- &b true\n'
+ '- &b true\n'
+ '- &b true\n'
+ '- &s hello\n'
+ '- *b\n'
+ '- *i\n' # declared in a previous document
+ '- *f_m\n' # never declared
+ '- *f_m\n'
+ '- *f_m\n'
+ '- *f_s\n' # declared after
+ '- &f_s [1, 2]\n'
+ '...\n'
+ '---\n'
+ 'block mapping: &b_m\n'
+ ' key: value\n'
+ '---\n'
+ 'block mapping 1: &b_m_bis\n'
+ ' key: value\n'
+ 'block mapping 2: &b_m_bis\n'
+ ' key: value\n'
+ 'extended:\n'
+ ' <<: *b_m\n'
+ ' foo: bar\n'
+ '---\n'
+ '{a: 1, &x b: 2, c: &x 3, *x : 4, e: *y}\n'
+ '...\n', conf,
+ problem1=(5, 3),
+ problem2=(6, 3),
+ problem3=(22, 18),
+ problem4=(28, 20))
+
+ def test_forbid_unused_anchors(self):
+ conf = ('anchors:\n'
+ ' forbid-undeclared-aliases: false\n'
+ ' forbid-duplicated-anchors: false\n'
+ ' forbid-unused-anchors: true\n')
+
+ self.check('---\n'
+ '- &b true\n'
+ '- &i 42\n'
+ '- &s hello\n'
+ '- &f_m {k: v}\n'
+ '- &f_s [1, 2]\n'
+ '- *b\n'
+ '- *i\n'
+ '- *s\n'
+ '- *f_m\n'
+ '- *f_s\n'
+ '---\n' # redeclare anchors in a new document
+ '- &b true\n'
+ '- &i 42\n'
+ '- &s hello\n'
+ '- *b\n'
+ '- *i\n'
+ '- *s\n'
+ '---\n'
+ 'block mapping: &b_m\n'
+ ' key: value\n'
+ 'extended:\n'
+ ' <<: *b_m\n'
+ ' foo: bar\n'
+ '---\n'
+ '{a: 1, &x b: 2, c: &y 3, *x : 4, e: *y}\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- &i 42\n'
+ '---\n'
+ '- &b true\n'
+ '- &b true\n'
+ '- &b true\n'
+ '- &s hello\n'
+ '- *b\n'
+ '- *i\n' # declared in a previous document
+ '- *f_m\n' # never declared
+ '- *f_m\n'
+ '- *f_m\n'
+ '- *f_s\n' # declared after
+ '- &f_s [1, 2]\n'
+ '...\n'
+ '---\n'
+ 'block mapping: &b_m\n'
+ ' key: value\n'
+ '---\n'
+ 'block mapping 1: &b_m_bis\n'
+ ' key: value\n'
+ 'block mapping 2: &b_m_bis\n'
+ ' key: value\n'
+ 'extended:\n'
+ ' <<: *b_m\n'
+ ' foo: bar\n'
+ '---\n'
+ '{a: 1, &x b: 2, c: &x 3, *x : 4, e: *y}\n'
+ '...\n', conf,
+ problem1=(2, 3),
+ problem2=(7, 3),
+ problem3=(14, 3),
+ problem4=(17, 16),
+ problem5=(22, 18))
diff --git a/tests/rules/test_braces.py b/tests/rules/test_braces.py
new file mode 100644
index 0000000..03636a9
--- /dev/null
+++ b/tests/rules/test_braces.py
@@ -0,0 +1,340 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class ColonTestCase(RuleTestCase):
+ rule_id = 'braces'
+
+ def test_disabled(self):
+ conf = 'braces: disable'
+ self.check('---\n'
+ 'dict1: {}\n'
+ 'dict2: { }\n'
+ 'dict3: { a: 1, b}\n'
+ 'dict4: {a: 1, b, c: 3 }\n'
+ 'dict5: {a: 1, b, c: 3 }\n'
+ 'dict6: { a: 1, b, c: 3 }\n'
+ 'dict7: { a: 1, b, c: 3 }\n', conf)
+
+ def test_forbid(self):
+ conf = ('braces:\n'
+ ' forbid: false\n')
+ self.check('---\n'
+ 'dict: {}\n', conf)
+ self.check('---\n'
+ 'dict: {a}\n', conf)
+ self.check('---\n'
+ 'dict: {a: 1}\n', conf)
+ self.check('---\n'
+ 'dict: {\n'
+ ' a: 1\n'
+ '}\n', conf)
+
+ conf = ('braces:\n'
+ ' forbid: true\n')
+ self.check('---\n'
+ 'dict:\n'
+ ' a: 1\n', conf)
+ self.check('---\n'
+ 'dict: {}\n', conf, problem=(2, 8))
+ self.check('---\n'
+ 'dict: {a}\n', conf, problem=(2, 8))
+ self.check('---\n'
+ 'dict: {a: 1}\n', conf, problem=(2, 8))
+ self.check('---\n'
+ 'dict: {\n'
+ ' a: 1\n'
+ '}\n', conf, problem=(2, 8))
+
+ conf = ('braces:\n'
+ ' forbid: non-empty\n')
+ self.check('---\n'
+ 'dict:\n'
+ ' a: 1\n', conf)
+ self.check('---\n'
+ 'dict: {}\n', conf)
+ self.check('---\n'
+ 'dict: {\n'
+ '}\n', conf)
+ self.check('---\n'
+ 'dict: {\n'
+ '# commented: value\n'
+ '# another: value2\n'
+ '}\n', conf)
+ self.check('---\n'
+ 'dict: {a}\n', conf, problem=(2, 8))
+ self.check('---\n'
+ 'dict: {a: 1}\n', conf, problem=(2, 8))
+ self.check('---\n'
+ 'dict: {\n'
+ ' a: 1\n'
+ '}\n', conf, problem=(2, 8))
+
+ def test_min_spaces(self):
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: 0\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'dict: {}\n', conf)
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'dict: {}\n', conf, problem=(2, 8))
+ self.check('---\n'
+ 'dict: { }\n', conf)
+ self.check('---\n'
+ 'dict: {a: 1, b}\n', conf,
+ problem1=(2, 8), problem2=(2, 15))
+ self.check('---\n'
+ 'dict: { a: 1, b }\n', conf)
+ self.check('---\n'
+ 'dict: {\n'
+ ' a: 1,\n'
+ ' b\n'
+ '}\n', conf)
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: 3\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'dict: { a: 1, b }\n', conf,
+ problem1=(2, 9), problem2=(2, 17))
+ self.check('---\n'
+ 'dict: { a: 1, b }\n', conf)
+
+ def test_max_spaces(self):
+ conf = ('braces:\n'
+ ' max-spaces-inside: 0\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'dict: {}\n', conf)
+ self.check('---\n'
+ 'dict: { }\n', conf, problem=(2, 8))
+ self.check('---\n'
+ 'dict: {a: 1, b}\n', conf)
+ self.check('---\n'
+ 'dict: { a: 1, b }\n', conf,
+ problem1=(2, 8), problem2=(2, 16))
+ self.check('---\n'
+ 'dict: { a: 1, b }\n', conf,
+ problem1=(2, 10), problem2=(2, 20))
+ self.check('---\n'
+ 'dict: {\n'
+ ' a: 1,\n'
+ ' b\n'
+ '}\n', conf)
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: 3\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'dict: { a: 1, b }\n', conf)
+ self.check('---\n'
+ 'dict: { a: 1, b }\n', conf,
+ problem1=(2, 11), problem2=(2, 23))
+
+ def test_min_and_max_spaces(self):
+ conf = ('braces:\n'
+ ' max-spaces-inside: 0\n'
+ ' min-spaces-inside: 0\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'dict: {}\n', conf)
+ self.check('---\n'
+ 'dict: { }\n', conf, problem=(2, 8))
+ self.check('---\n'
+ 'dict: { a: 1, b}\n', conf, problem=(2, 10))
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: 1\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'dict: {a: 1, b, c: 3 }\n', conf, problem=(2, 8))
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: 2\n'
+ ' min-spaces-inside: 0\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'dict: {a: 1, b, c: 3 }\n', conf)
+ self.check('---\n'
+ 'dict: { a: 1, b, c: 3 }\n', conf)
+ self.check('---\n'
+ 'dict: { a: 1, b, c: 3 }\n', conf, problem=(2, 10))
+
+ def test_min_spaces_empty(self):
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 0\n'
+ ' min-spaces-inside-empty: 0\n')
+ self.check('---\n'
+ 'array: {}\n', conf)
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: {}\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: { }\n', conf)
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: 3\n')
+ self.check('---\n'
+ 'array: {}\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: { }\n', conf)
+
+ def test_max_spaces_empty(self):
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 0\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: {}\n', conf)
+ self.check('---\n'
+ 'array: { }\n', conf, problem=(2, 9))
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: {}\n', conf)
+ self.check('---\n'
+ 'array: { }\n', conf)
+ self.check('---\n'
+ 'array: { }\n', conf, problem=(2, 10))
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 3\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: {}\n', conf)
+ self.check('---\n'
+ 'array: { }\n', conf)
+ self.check('---\n'
+ 'array: { }\n', conf, problem=(2, 12))
+
+ def test_min_and_max_spaces_empty(self):
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 2\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: {}\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: { }\n', conf)
+ self.check('---\n'
+ 'array: { }\n', conf)
+ self.check('---\n'
+ 'array: { }\n', conf, problem=(2, 11))
+
+ def test_mixed_empty_nonempty(self):
+ conf = ('braces:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: 0\n'
+ ' min-spaces-inside-empty: 0\n')
+ self.check('---\n'
+ 'array: { a: 1, b }\n', conf)
+ self.check('---\n'
+ 'array: {a: 1, b}\n', conf,
+ problem1=(2, 9), problem2=(2, 16))
+ self.check('---\n'
+ 'array: {}\n', conf)
+ self.check('---\n'
+ 'array: { }\n', conf,
+ problem1=(2, 9))
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: 0\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 1\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: { a: 1, b }\n', conf,
+ problem1=(2, 9), problem2=(2, 17))
+ self.check('---\n'
+ 'array: {a: 1, b}\n', conf)
+ self.check('---\n'
+ 'array: {}\n', conf,
+ problem1=(2, 9))
+ self.check('---\n'
+ 'array: { }\n', conf)
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: 2\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: 1\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: { a: 1, b }\n', conf)
+ self.check('---\n'
+ 'array: {a: 1, b }\n', conf,
+ problem1=(2, 9), problem2=(2, 18))
+ self.check('---\n'
+ 'array: {}\n', conf,
+ problem1=(2, 9))
+ self.check('---\n'
+ 'array: { }\n', conf)
+ self.check('---\n'
+ 'array: { }\n', conf,
+ problem1=(2, 11))
+
+ conf = ('braces:\n'
+ ' max-spaces-inside: 1\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: 1\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: { a: 1, b }\n', conf)
+ self.check('---\n'
+ 'array: {a: 1, b}\n', conf,
+ problem1=(2, 9), problem2=(2, 16))
+ self.check('---\n'
+ 'array: {}\n', conf,
+ problem1=(2, 9))
+ self.check('---\n'
+ 'array: { }\n', conf)
diff --git a/tests/rules/test_brackets.py b/tests/rules/test_brackets.py
new file mode 100644
index 0000000..e17566b
--- /dev/null
+++ b/tests/rules/test_brackets.py
@@ -0,0 +1,337 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class ColonTestCase(RuleTestCase):
+ rule_id = 'brackets'
+
+ def test_disabled(self):
+ conf = 'brackets: disable'
+ self.check('---\n'
+ 'array1: []\n'
+ 'array2: [ ]\n'
+ 'array3: [ a, b]\n'
+ 'array4: [a, b, c ]\n'
+ 'array5: [a, b, c ]\n'
+ 'array6: [ a, b, c ]\n'
+ 'array7: [ a, b, c ]\n', conf)
+
+ def test_forbid(self):
+ conf = ('brackets:\n'
+ ' forbid: false\n')
+ self.check('---\n'
+ 'array: []\n', conf)
+ self.check('---\n'
+ 'array: [a, b]\n', conf)
+ self.check('---\n'
+ 'array: [\n'
+ ' a,\n'
+ ' b\n'
+ ']\n', conf)
+
+ conf = ('brackets:\n'
+ ' forbid: true\n')
+ self.check('---\n'
+ 'array:\n'
+ ' - a\n'
+ ' - b\n', conf)
+ self.check('---\n'
+ 'array: []\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: [a, b]\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: [\n'
+ ' a,\n'
+ ' b\n'
+ ']\n', conf, problem=(2, 9))
+
+ conf = ('brackets:\n'
+ ' forbid: non-empty\n')
+ self.check('---\n'
+ 'array:\n'
+ ' - a\n'
+ ' - b\n', conf)
+ self.check('---\n'
+ 'array: []\n', conf)
+ self.check('---\n'
+ 'array: [\n\n'
+ ']\n', conf)
+ self.check('---\n'
+ 'array: [\n'
+ '# a comment\n'
+ ']\n', conf)
+ self.check('---\n'
+ 'array: [a, b]\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: [\n'
+ ' a,\n'
+ ' b\n'
+ ']\n', conf, problem=(2, 9))
+
+ def test_min_spaces(self):
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: 0\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: []\n', conf)
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: []\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: [ ]\n', conf)
+ self.check('---\n'
+ 'array: [a, b]\n', conf, problem1=(2, 9), problem2=(2, 13))
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf)
+ self.check('---\n'
+ 'array: [\n'
+ ' a,\n'
+ ' b\n'
+ ']\n', conf)
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: 3\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf,
+ problem1=(2, 10), problem2=(2, 15))
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf)
+
+ def test_max_spaces(self):
+ conf = ('brackets:\n'
+ ' max-spaces-inside: 0\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: []\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: [a, b]\n', conf)
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf,
+ problem1=(2, 9), problem2=(2, 14))
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf,
+ problem1=(2, 11), problem2=(2, 18))
+ self.check('---\n'
+ 'array: [\n'
+ ' a,\n'
+ ' b\n'
+ ']\n', conf)
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: 3\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf)
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf,
+ problem1=(2, 12), problem2=(2, 21))
+
+ def test_min_and_max_spaces(self):
+ conf = ('brackets:\n'
+ ' max-spaces-inside: 0\n'
+ ' min-spaces-inside: 0\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: []\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: [ a, b]\n', conf, problem=(2, 11))
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: 1\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: [a, b, c ]\n', conf, problem=(2, 9))
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: 2\n'
+ ' min-spaces-inside: 0\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: [a, b, c ]\n', conf)
+ self.check('---\n'
+ 'array: [ a, b, c ]\n', conf)
+ self.check('---\n'
+ 'array: [ a, b, c ]\n', conf, problem=(2, 11))
+
+ def test_min_spaces_empty(self):
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 0\n'
+ ' min-spaces-inside-empty: 0\n')
+ self.check('---\n'
+ 'array: []\n', conf)
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: []\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: [ ]\n', conf)
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: -1\n'
+ ' min-spaces-inside-empty: 3\n')
+ self.check('---\n'
+ 'array: []\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: [ ]\n', conf)
+
+ def test_max_spaces_empty(self):
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 0\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: []\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf, problem=(2, 9))
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 1\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: []\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf, problem=(2, 10))
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 3\n'
+ ' min-spaces-inside-empty: -1\n')
+ self.check('---\n'
+ 'array: []\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf, problem=(2, 12))
+
+ def test_min_and_max_spaces_empty(self):
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 2\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: []\n', conf, problem=(2, 9))
+ self.check('---\n'
+ 'array: [ ]\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf, problem=(2, 11))
+
+ def test_mixed_empty_nonempty(self):
+ conf = ('brackets:\n'
+ ' max-spaces-inside: -1\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: 0\n'
+ ' min-spaces-inside-empty: 0\n')
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf)
+ self.check('---\n'
+ 'array: [a, b]\n', conf,
+ problem1=(2, 9), problem2=(2, 13))
+ self.check('---\n'
+ 'array: []\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf,
+ problem1=(2, 9))
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: 0\n'
+ ' min-spaces-inside: -1\n'
+ ' max-spaces-inside-empty: 1\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf,
+ problem1=(2, 9), problem2=(2, 14))
+ self.check('---\n'
+ 'array: [a, b]\n', conf)
+ self.check('---\n'
+ 'array: []\n', conf,
+ problem1=(2, 9))
+ self.check('---\n'
+ 'array: [ ]\n', conf)
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: 2\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: 1\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf)
+ self.check('---\n'
+ 'array: [a, b ]\n', conf,
+ problem1=(2, 9), problem2=(2, 15))
+ self.check('---\n'
+ 'array: []\n', conf,
+ problem1=(2, 9))
+ self.check('---\n'
+ 'array: [ ]\n', conf)
+ self.check('---\n'
+ 'array: [ ]\n', conf,
+ problem1=(2, 11))
+
+ conf = ('brackets:\n'
+ ' max-spaces-inside: 1\n'
+ ' min-spaces-inside: 1\n'
+ ' max-spaces-inside-empty: 1\n'
+ ' min-spaces-inside-empty: 1\n')
+ self.check('---\n'
+ 'array: [ a, b ]\n', conf)
+ self.check('---\n'
+ 'array: [a, b]\n', conf,
+ problem1=(2, 9), problem2=(2, 13))
+ self.check('---\n'
+ 'array: []\n', conf,
+ problem1=(2, 9))
+ self.check('---\n'
+ 'array: [ ]\n', conf)
diff --git a/tests/rules/test_colons.py b/tests/rules/test_colons.py
new file mode 100644
index 0000000..5467c8b
--- /dev/null
+++ b/tests/rules/test_colons.py
@@ -0,0 +1,274 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class ColonTestCase(RuleTestCase):
+ rule_id = 'colons'
+
+ def test_disabled(self):
+ conf = 'colons: disable'
+ self.check('---\n'
+ 'object:\n'
+ ' k1 : v1\n'
+ 'obj2:\n'
+ ' k2 :\n'
+ ' - 8\n'
+ ' k3:\n'
+ ' val\n'
+ ' property : value\n'
+ ' prop2 : val2\n'
+ ' propriété : [valeur]\n'
+ ' o:\n'
+ ' k1: [v1, v2]\n'
+ ' p:\n'
+ ' - k3: >\n'
+ ' val\n'
+ ' - o: {k1: v1}\n'
+ ' - p: kdjf\n'
+ ' - q: val0\n'
+ ' - q2:\n'
+ ' - val1\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' k1: v1\n'
+ 'obj2:\n'
+ ' k2:\n'
+ ' - 8\n'
+ ' k3:\n'
+ ' val\n'
+ ' property: value\n'
+ ' prop2: val2\n'
+ ' propriété: [valeur]\n'
+ ' o:\n'
+ ' k1: [v1, v2]\n', conf)
+ self.check('---\n'
+ 'obj:\n'
+ ' p:\n'
+ ' - k1: >\n'
+ ' val\n'
+ ' - k3: >\n'
+ ' val\n'
+ ' - o: {k1: v1}\n'
+ ' - o: {k1: v1}\n'
+ ' - q2:\n'
+ ' - val1\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'a: {b: {c: d, e : f}}\n', conf)
+
+ def test_before_enabled(self):
+ conf = 'colons: {max-spaces-before: 0, max-spaces-after: -1}'
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' k1 :\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ '...\n', conf, problem=(3, 5))
+ self.check('---\n'
+ 'lib :\n'
+ ' - var\n'
+ '...\n', conf, problem=(2, 4))
+ self.check('---\n'
+ '- lib :\n'
+ ' - var\n'
+ '...\n', conf, problem=(2, 6))
+ self.check('---\n'
+ 'a: {b: {c : d, e : f}}\n', conf,
+ problem1=(2, 10), problem2=(2, 17))
+
+ def test_before_max(self):
+ conf = 'colons: {max-spaces-before: 3, max-spaces-after: -1}'
+ self.check('---\n'
+ 'object :\n'
+ ' k1 :\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2 : v2\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object :\n'
+ ' k1 :\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2 : v2\n'
+ '...\n', conf, problem=(3, 8))
+
+ def test_before_with_explicit_block_mappings(self):
+ conf = 'colons: {max-spaces-before: 0, max-spaces-after: 1}'
+ self.check('---\n'
+ 'object:\n'
+ ' ? key\n'
+ ' : value\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object :\n'
+ ' ? key\n'
+ ' : value\n'
+ '...\n', conf, problem=(2, 7))
+ self.check('---\n'
+ '? >\n'
+ ' multi-line\n'
+ ' key\n'
+ ': >\n'
+ ' multi-line\n'
+ ' value\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- ? >\n'
+ ' multi-line\n'
+ ' key\n'
+ ' : >\n'
+ ' multi-line\n'
+ ' value\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- ? >\n'
+ ' multi-line\n'
+ ' key\n'
+ ' : >\n'
+ ' multi-line\n'
+ ' value\n'
+ '...\n', conf, problem=(5, 5))
+
+ def test_after_enabled(self):
+ conf = 'colons: {max-spaces-before: -1, max-spaces-after: 1}'
+ self.check('---\n'
+ 'key: value\n', conf)
+ self.check('---\n'
+ 'key: value\n', conf, problem=(2, 6))
+ self.check('---\n'
+ 'object:\n'
+ ' k1: [a, b]\n'
+ ' k2: string\n', conf, problem=(3, 7))
+ self.check('---\n'
+ 'object:\n'
+ ' k1: [a, b]\n'
+ ' k2: string\n', conf, problem=(4, 7))
+ self.check('---\n'
+ 'object:\n'
+ ' other: {key: value}\n'
+ '...\n', conf, problem=(3, 16))
+ self.check('---\n'
+ 'a: {b: {c: d, e : f}}\n', conf,
+ problem1=(2, 12), problem2=(2, 20))
+
+ def test_after_enabled_question_mark(self):
+ conf = 'colons: {max-spaces-before: -1, max-spaces-after: 1}'
+ self.check('---\n'
+ '? key\n'
+ ': value\n', conf)
+ self.check('---\n'
+ '? key\n'
+ ': value\n', conf, problem=(2, 3))
+ self.check('---\n'
+ '? key\n'
+ ': value\n', conf, problem1=(2, 3), problem2=(3, 3))
+ self.check('---\n'
+ '- ? key\n'
+ ' : value\n', conf, problem1=(2, 5), problem2=(3, 5))
+
+ def test_after_max(self):
+ conf = 'colons: {max-spaces-before: -1, max-spaces-after: 3}'
+ self.check('---\n'
+ 'object:\n'
+ ' k1: [a, b]\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' k1: [a, b]\n', conf, problem=(3, 9))
+ self.check('---\n'
+ 'object:\n'
+ ' k2: string\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' k2: string\n', conf, problem=(3, 9))
+ self.check('---\n'
+ 'object:\n'
+ ' other: {key: value}\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' other: {key: value}\n'
+ '...\n', conf, problem=(3, 18))
+
+ def test_after_with_explicit_block_mappings(self):
+ conf = 'colons: {max-spaces-before: -1, max-spaces-after: 1}'
+ self.check('---\n'
+ 'object:\n'
+ ' ? key\n'
+ ' : value\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' ? key\n'
+ ' : value\n'
+ '...\n', conf, problem=(4, 5))
+
+ def test_after_do_not_confound_with_trailing_space(self):
+ conf = ('colons: {max-spaces-before: 1, max-spaces-after: 1}\n'
+ 'trailing-spaces: disable\n')
+ self.check('---\n'
+ 'trailing: \n'
+ ' - spaces\n', conf)
+
+ def test_both_before_and_after(self):
+ conf = 'colons: {max-spaces-before: 0, max-spaces-after: 1}'
+ self.check('---\n'
+ 'obj:\n'
+ ' string: text\n'
+ ' k:\n'
+ ' - 8\n'
+ ' k3:\n'
+ ' val\n'
+ ' property: [value]\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' k1 : v1\n', conf, problem1=(3, 5), problem2=(3, 8))
+ self.check('---\n'
+ 'obj:\n'
+ ' string: text\n'
+ ' k :\n'
+ ' - 8\n'
+ ' k3:\n'
+ ' val\n'
+ ' property: {a: 1, b: 2, c : 3}\n', conf,
+ problem1=(3, 11), problem2=(4, 4),
+ problem3=(8, 23), problem4=(8, 28))
+
+ # Although accepted by PyYAML, `{*x: 4}` is not valid YAML: it should be
+ # noted `{*x : 4}`. The reason is that a colon can be part of an anchor
+ # name. See commit message for more details.
+ def test_with_alias_as_key(self):
+ conf = 'colons: {max-spaces-before: 0, max-spaces-after: 1}'
+ self.check('---\n'
+ '- anchor: &a key\n'
+ '- *a: 42\n'
+ '- {*a: 42}\n'
+ '- *a : 42\n'
+ '- {*a : 42}\n'
+ '- *a : 42\n'
+ '- {*a : 42}\n',
+ conf,
+ problem1=(7, 6), problem2=(8, 7))
diff --git a/tests/rules/test_commas.py b/tests/rules/test_commas.py
new file mode 100644
index 0000000..0d0abd8
--- /dev/null
+++ b/tests/rules/test_commas.py
@@ -0,0 +1,264 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class CommaTestCase(RuleTestCase):
+ rule_id = 'commas'
+
+ def test_disabled(self):
+ conf = 'commas: disable'
+ self.check('---\n'
+ 'dict: {a: b , c: "1 2 3", d: e , f: [g, h]}\n'
+ 'array: [\n'
+ ' elem ,\n'
+ ' key: val ,\n'
+ ']\n'
+ 'map: {\n'
+ ' key1: val1 ,\n'
+ ' key2: val2,\n'
+ '}\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- [one, two , three,four]\n'
+ '- {five,six , seven, eight}\n'
+ '- [\n'
+ ' nine, ten\n'
+ ' , eleven\n'
+ ' ,twelve\n'
+ ']\n'
+ '- {\n'
+ ' thirteen: 13, fourteen\n'
+ ' , fifteen: 15\n'
+ ' ,sixteen: 16\n'
+ '}\n', conf)
+
+ def test_before_max(self):
+ conf = ('commas:\n'
+ ' max-spaces-before: 0\n'
+ ' min-spaces-after: 0\n'
+ ' max-spaces-after: -1\n')
+ self.check('---\n'
+ 'array: [1, 2, 3, 4]\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'array: [1, 2 , 3, 4]\n'
+ '...\n', conf, problem=(2, 13))
+ self.check('---\n'
+ 'array: [1 , 2, 3 , 4]\n'
+ '...\n', conf, problem1=(2, 10), problem2=(2, 23))
+ self.check('---\n'
+ 'dict: {a: b, c: "1 2 3", d: e, f: [g, h]}\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'dict: {a: b, c: "1 2 3" , d: e, f: [g, h]}\n'
+ '...\n', conf, problem=(2, 24))
+ self.check('---\n'
+ 'dict: {a: b , c: "1 2 3", d: e, f: [g , h]}\n'
+ '...\n', conf, problem1=(2, 12), problem2=(2, 42))
+ self.check('---\n'
+ 'array: [\n'
+ ' elem,\n'
+ ' key: val,\n'
+ ']\n', conf)
+ self.check('---\n'
+ 'array: [\n'
+ ' elem ,\n'
+ ' key: val,\n'
+ ']\n', conf, problem=(3, 7))
+ self.check('---\n'
+ 'map: {\n'
+ ' key1: val1,\n'
+ ' key2: val2,\n'
+ '}\n', conf)
+ self.check('---\n'
+ 'map: {\n'
+ ' key1: val1,\n'
+ ' key2: val2 ,\n'
+ '}\n', conf, problem=(4, 13))
+
+ def test_before_max_with_comma_on_new_line(self):
+ conf = ('commas:\n'
+ ' max-spaces-before: 0\n'
+ ' min-spaces-after: 0\n'
+ ' max-spaces-after: -1\n')
+ self.check('---\n'
+ 'flow-seq: [1, 2, 3\n'
+ ' , 4, 5, 6]\n'
+ '...\n', conf, problem=(3, 11))
+ self.check('---\n'
+ 'flow-map: {a: 1, b: 2\n'
+ ' , c: 3}\n'
+ '...\n', conf, problem=(3, 11))
+
+ conf = ('commas:\n'
+ ' max-spaces-before: 0\n'
+ ' min-spaces-after: 0\n'
+ ' max-spaces-after: -1\n'
+ 'indentation: disable\n')
+ self.check('---\n'
+ 'flow-seq: [1, 2, 3\n'
+ ' , 4, 5, 6]\n'
+ '...\n', conf, problem=(3, 9))
+ self.check('---\n'
+ 'flow-map: {a: 1, b: 2\n'
+ ' , c: 3}\n'
+ '...\n', conf, problem=(3, 9))
+ self.check('---\n'
+ '[\n'
+ '1,\n'
+ '2\n'
+ ', 3\n'
+ ']\n', conf, problem=(5, 1))
+ self.check('---\n'
+ '{\n'
+ 'a: 1,\n'
+ 'b: 2\n'
+ ', c: 3\n'
+ '}\n', conf, problem=(5, 1))
+
+ def test_before_max_3(self):
+ conf = ('commas:\n'
+ ' max-spaces-before: 3\n'
+ ' min-spaces-after: 0\n'
+ ' max-spaces-after: -1\n')
+ self.check('---\n'
+ 'array: [1 , 2, 3 , 4]\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'array: [1 , 2, 3 , 4]\n'
+ '...\n', conf, problem=(2, 20))
+ self.check('---\n'
+ 'array: [\n'
+ ' elem1 ,\n'
+ ' elem2 ,\n'
+ ' key: val,\n'
+ ']\n', conf, problem=(4, 11))
+
+ def test_after_min(self):
+ conf = ('commas:\n'
+ ' max-spaces-before: -1\n'
+ ' min-spaces-after: 1\n'
+ ' max-spaces-after: -1\n')
+ self.check('---\n'
+ '- [one, two , three,four]\n'
+ '- {five,six , seven, eight}\n'
+ '- [\n'
+ ' nine, ten\n'
+ ' , eleven\n'
+ ' ,twelve\n'
+ ']\n'
+ '- {\n'
+ ' thirteen: 13, fourteen\n'
+ ' , fifteen: 15\n'
+ ' ,sixteen: 16\n'
+ '}\n', conf,
+ problem1=(2, 21), problem2=(3, 9),
+ problem3=(7, 4), problem4=(12, 4))
+
+ def test_after_max(self):
+ conf = ('commas:\n'
+ ' max-spaces-before: -1\n'
+ ' min-spaces-after: 0\n'
+ ' max-spaces-after: 1\n')
+ self.check('---\n'
+ 'array: [1, 2, 3, 4]\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'array: [1, 2, 3, 4]\n'
+ '...\n', conf, problem=(2, 15))
+ self.check('---\n'
+ 'array: [1, 2, 3, 4]\n'
+ '...\n', conf, problem1=(2, 12), problem2=(2, 22))
+ self.check('---\n'
+ 'dict: {a: b , c: "1 2 3", d: e, f: [g, h]}\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'dict: {a: b , c: "1 2 3", d: e, f: [g, h]}\n'
+ '...\n', conf, problem=(2, 27))
+ self.check('---\n'
+ 'dict: {a: b , c: "1 2 3", d: e, f: [g, h]}\n'
+ '...\n', conf, problem1=(2, 15), problem2=(2, 44))
+ self.check('---\n'
+ 'array: [\n'
+ ' elem,\n'
+ ' key: val,\n'
+ ']\n', conf)
+ self.check('---\n'
+ 'array: [\n'
+ ' elem, key: val,\n'
+ ']\n', conf, problem=(3, 9))
+ self.check('---\n'
+ 'map: {\n'
+ ' key1: val1, key2: [val2, val3]\n'
+ '}\n', conf, problem1=(3, 16), problem2=(3, 30))
+
+ def test_after_max_3(self):
+ conf = ('commas:\n'
+ ' max-spaces-before: -1\n'
+ ' min-spaces-after: 1\n'
+ ' max-spaces-after: 3\n')
+ self.check('---\n'
+ 'array: [1, 2, 3, 4]\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'array: [1, 2, 3, 4]\n'
+ '...\n', conf, problem=(2, 21))
+ self.check('---\n'
+ 'dict: {a: b , c: "1 2 3", d: e, f: [g, h]}\n'
+ '...\n', conf, problem1=(2, 31), problem2=(2, 49))
+
+ def test_both_before_and_after(self):
+ conf = ('commas:\n'
+ ' max-spaces-before: 0\n'
+ ' min-spaces-after: 1\n'
+ ' max-spaces-after: 1\n')
+ self.check('---\n'
+ 'dict: {a: b , c: "1 2 3", d: e , f: [g, h]}\n'
+ 'array: [\n'
+ ' elem ,\n'
+ ' key: val ,\n'
+ ']\n'
+ 'map: {\n'
+ ' key1: val1 ,\n'
+ ' key2: val2,\n'
+ '}\n'
+ '...\n', conf,
+ problem1=(2, 12), problem2=(2, 16), problem3=(2, 31),
+ problem4=(2, 36), problem5=(2, 50), problem6=(4, 8),
+ problem7=(5, 11), problem8=(8, 13))
+ conf = ('commas:\n'
+ ' max-spaces-before: 0\n'
+ ' min-spaces-after: 1\n'
+ ' max-spaces-after: 1\n'
+ 'indentation: disable\n')
+ self.check('---\n'
+ '- [one, two , three,four]\n'
+ '- {five,six , seven, eight}\n'
+ '- [\n'
+ ' nine, ten\n'
+ ' , eleven\n'
+ ' ,twelve\n'
+ ']\n'
+ '- {\n'
+ ' thirteen: 13, fourteen\n'
+ ' , fifteen: 15\n'
+ ' ,sixteen: 16\n'
+ '}\n', conf,
+ problem1=(2, 12), problem2=(2, 21), problem3=(3, 9),
+ problem4=(3, 12), problem5=(5, 9), problem6=(6, 2),
+ problem7=(7, 2), problem8=(7, 4), problem9=(10, 17),
+ problem10=(11, 2), problem11=(12, 2), problem12=(12, 4))
diff --git a/tests/rules/test_comments.py b/tests/rules/test_comments.py
new file mode 100644
index 0000000..1f5a20c
--- /dev/null
+++ b/tests/rules/test_comments.py
@@ -0,0 +1,236 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class CommentsTestCase(RuleTestCase):
+ rule_id = 'comments'
+
+ def test_disabled(self):
+ conf = ('comments: disable\n'
+ 'comments-indentation: disable\n')
+ self.check('---\n'
+ '#comment\n'
+ '\n'
+ 'test: # description\n'
+ ' - foo # bar\n'
+ ' - hello #world\n'
+ '\n'
+ '# comment 2\n'
+ '#comment 3\n'
+ ' #comment 3 bis\n'
+ ' # comment 3 ter\n'
+ '\n'
+ '################################\n'
+ '## comment 4\n'
+ '##comment 5\n'
+ '\n'
+ 'string: "Une longue phrase." # this is French\n', conf)
+
+ def test_starting_space(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' min-spaces-from-content: -1\n'
+ 'comments-indentation: disable\n')
+ self.check('---\n'
+ '# comment\n'
+ '\n'
+ 'test: # description\n'
+ ' - foo # bar\n'
+ ' - hello # world\n'
+ '\n'
+ '# comment 2\n'
+ '# comment 3\n'
+ ' # comment 3 bis\n'
+ ' # comment 3 ter\n'
+ '\n'
+ '################################\n'
+ '## comment 4\n'
+ '## comment 5\n', conf)
+ self.check('---\n'
+ '#comment\n'
+ '\n'
+ 'test: # description\n'
+ ' - foo # bar\n'
+ ' - hello #world\n'
+ '\n'
+ '# comment 2\n'
+ '#comment 3\n'
+ ' #comment 3 bis\n'
+ ' # comment 3 ter\n'
+ '\n'
+ '################################\n'
+ '## comment 4\n'
+ '##comment 5\n', conf,
+ problem1=(2, 2), problem2=(6, 13),
+ problem3=(9, 2), problem4=(10, 4),
+ problem5=(15, 3))
+
+ def test_shebang(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' ignore-shebangs: false\n'
+ 'comments-indentation: disable\n'
+ 'document-start: disable\n')
+ self.check('#!/bin/env my-interpreter\n',
+ conf, problem1=(1, 2))
+ self.check('# comment\n'
+ '#!/bin/env my-interpreter\n', conf,
+ problem1=(2, 2))
+ self.check('#!/bin/env my-interpreter\n'
+ '---\n'
+ '#comment\n'
+ '#!/bin/env my-interpreter\n'
+ '', conf,
+ problem1=(1, 2), problem2=(3, 2), problem3=(4, 2))
+ self.check('#! is a valid shebang too\n',
+ conf, problem1=(1, 2))
+ self.check('key: #!/not/a/shebang\n',
+ conf, problem1=(1, 8))
+
+ def test_ignore_shebang(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' ignore-shebangs: true\n'
+ 'comments-indentation: disable\n'
+ 'document-start: disable\n')
+ self.check('#!/bin/env my-interpreter\n', conf)
+ self.check('# comment\n'
+ '#!/bin/env my-interpreter\n', conf,
+ problem1=(2, 2))
+ self.check('#!/bin/env my-interpreter\n'
+ '---\n'
+ '#comment\n'
+ '#!/bin/env my-interpreter\n', conf,
+ problem2=(3, 2), problem3=(4, 2))
+ self.check('#! is a valid shebang too\n', conf)
+ self.check('key: #!/not/a/shebang\n',
+ conf, problem1=(1, 8))
+
+ def test_spaces_from_content(self):
+ conf = ('comments:\n'
+ ' require-starting-space: false\n'
+ ' min-spaces-from-content: 2\n')
+ self.check('---\n'
+ '# comment\n'
+ '\n'
+ 'test: # description\n'
+ ' - foo # bar\n'
+ ' - hello #world\n'
+ '\n'
+ 'string: "Une longue phrase." # this is French\n', conf)
+ self.check('---\n'
+ '# comment\n'
+ '\n'
+ 'test: # description\n'
+ ' - foo # bar\n'
+ ' - hello #world\n'
+ '\n'
+ 'string: "Une longue phrase." # this is French\n', conf,
+ problem1=(4, 7), problem2=(6, 11), problem3=(8, 30))
+
+ def test_both(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' min-spaces-from-content: 2\n'
+ 'comments-indentation: disable\n')
+ self.check('---\n'
+ '#comment\n'
+ '\n'
+ 'test: # description\n'
+ ' - foo # bar\n'
+ ' - hello #world\n'
+ '\n'
+ '# comment 2\n'
+ '#comment 3\n'
+ ' #comment 3 bis\n'
+ ' # comment 3 ter\n'
+ '\n'
+ '################################\n'
+ '## comment 4\n'
+ '##comment 5\n'
+ '\n'
+ 'string: "Une longue phrase." # this is French\n', conf,
+ problem1=(2, 2),
+ problem2=(4, 7),
+ problem3=(6, 11), problem4=(6, 12),
+ problem5=(9, 2),
+ problem6=(10, 4),
+ problem7=(15, 3),
+ problem8=(17, 30))
+
+ def test_empty_comment(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' min-spaces-from-content: 2\n')
+ self.check('---\n'
+ '# This is paragraph 1.\n'
+ '#\n'
+ '# This is paragraph 2.\n', conf)
+ self.check('---\n'
+ 'inline: comment #\n'
+ 'foo: bar\n', conf)
+
+ def test_empty_comment_crlf_dos_newlines(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' min-spaces-from-content: 2\n'
+ 'new-lines:\n'
+ ' type: dos\n')
+ self.check('---\r\n'
+ '# This is paragraph 1.\r\n'
+ '#\r\n'
+ '# This is paragraph 2.\r\n', conf)
+
+ def test_empty_comment_crlf_disabled_newlines(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' min-spaces-from-content: 2\n'
+ 'new-lines: disable\n')
+ self.check('---\r\n'
+ '# This is paragraph 1.\r\n'
+ '#\r\n'
+ '# This is paragraph 2.\r\n', conf)
+
+ def test_first_line(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' min-spaces-from-content: 2\n')
+ self.check('# comment\n', conf)
+
+ def test_last_line(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' min-spaces-from-content: 2\n'
+ 'new-line-at-end-of-file: disable\n')
+ self.check('# comment with no newline char:\n'
+ '#', conf)
+
+ def test_multi_line_scalar(self):
+ conf = ('comments:\n'
+ ' require-starting-space: true\n'
+ ' min-spaces-from-content: 2\n'
+ 'trailing-spaces: disable\n')
+ self.check('---\n'
+ 'string: >\n'
+ ' this is plain text\n'
+ '\n'
+ '# comment\n', conf)
+ self.check('---\n'
+ '- string: >\n'
+ ' this is plain text\n'
+ ' \n'
+ ' # comment\n', conf)
diff --git a/tests/rules/test_comments_indentation.py b/tests/rules/test_comments_indentation.py
new file mode 100644
index 0000000..0aa5aac
--- /dev/null
+++ b/tests/rules/test_comments_indentation.py
@@ -0,0 +1,156 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class CommentsIndentationTestCase(RuleTestCase):
+ rule_id = 'comments-indentation'
+
+ def test_disable(self):
+ conf = 'comments-indentation: disable'
+ self.check('---\n'
+ ' # line 1\n'
+ '# line 2\n'
+ ' # line 3\n'
+ ' # line 4\n'
+ '\n'
+ 'obj:\n'
+ ' # these\n'
+ ' # are\n'
+ ' # [good]\n'
+ '# bad\n'
+ ' # comments\n'
+ ' a: b\n'
+ '\n'
+ 'obj1:\n'
+ ' a: 1\n'
+ ' # comments\n'
+ '\n'
+ 'obj2:\n'
+ ' b: 2\n'
+ '\n'
+ '# empty\n'
+ '#\n'
+ '# comment\n'
+ '...\n', conf)
+
+ def test_enabled(self):
+ conf = 'comments-indentation: enable'
+ self.check('---\n'
+ '# line 1\n'
+ '# line 2\n', conf)
+ self.check('---\n'
+ ' # line 1\n'
+ '# line 2\n', conf, problem=(2, 2))
+ self.check('---\n'
+ ' # line 1\n'
+ ' # line 2\n', conf, problem1=(2, 3))
+ self.check('---\n'
+ 'obj:\n'
+ ' # normal\n'
+ ' a: b\n', conf)
+ self.check('---\n'
+ 'obj:\n'
+ ' # bad\n'
+ ' a: b\n', conf, problem=(3, 2))
+ self.check('---\n'
+ 'obj:\n'
+ '# bad\n'
+ ' a: b\n', conf, problem=(3, 1))
+ self.check('---\n'
+ 'obj:\n'
+ ' # bad\n'
+ ' a: b\n', conf, problem=(3, 4))
+ self.check('---\n'
+ 'obj:\n'
+ ' # these\n'
+ ' # are\n'
+ ' # [good]\n'
+ '# bad\n'
+ ' # comments\n'
+ ' a: b\n', conf,
+ problem1=(3, 2), problem2=(4, 4),
+ problem3=(6, 1), problem4=(7, 7))
+ self.check('---\n'
+ 'obj1:\n'
+ ' a: 1\n'
+ ' # the following line is disabled\n'
+ ' # b: 2\n', conf)
+ self.check('---\n'
+ 'obj1:\n'
+ ' a: 1\n'
+ ' # b: 2\n'
+ '\n'
+ 'obj2:\n'
+ ' b: 2\n', conf)
+ self.check('---\n'
+ 'obj1:\n'
+ ' a: 1\n'
+ ' # b: 2\n'
+ '# this object is useless\n'
+ 'obj2: "no"\n', conf)
+ self.check('---\n'
+ 'obj1:\n'
+ ' a: 1\n'
+ '# this object is useless\n'
+ ' # b: 2\n'
+ 'obj2: "no"\n', conf, problem=(5, 3))
+ self.check('---\n'
+ 'obj1:\n'
+ ' a: 1\n'
+ ' # comments\n'
+ ' b: 2\n', conf)
+ self.check('---\n'
+ 'my list for today:\n'
+ ' - todo 1\n'
+ ' - todo 2\n'
+ ' # commented for now\n'
+ ' # - todo 3\n'
+ '...\n', conf)
+
+ def test_first_line(self):
+ conf = 'comments-indentation: enable'
+ self.check('# comment\n', conf)
+ self.check(' # comment\n', conf, problem=(1, 3))
+
+ def test_no_newline_at_end(self):
+ conf = ('comments-indentation: enable\n'
+ 'new-line-at-end-of-file: disable\n')
+ self.check('# comment', conf)
+ self.check(' # comment', conf, problem=(1, 3))
+
+ def test_empty_comment(self):
+ conf = 'comments-indentation: enable'
+ self.check('---\n'
+ '# hey\n'
+ '# normal\n'
+ '#\n', conf)
+ self.check('---\n'
+ '# hey\n'
+ '# normal\n'
+ ' #\n', conf, problem=(4, 2))
+
+ def test_inline_comment(self):
+ conf = 'comments-indentation: enable'
+ self.check('---\n'
+ '- a # inline\n'
+ '# ok\n', conf)
+ self.check('---\n'
+ '- a # inline\n'
+ ' # not ok\n', conf, problem=(3, 2))
+ self.check('---\n'
+ ' # not ok\n'
+ '- a # inline\n', conf, problem=(2, 2))
diff --git a/tests/rules/test_common.py b/tests/rules/test_common.py
new file mode 100644
index 0000000..196b419
--- /dev/null
+++ b/tests/rules/test_common.py
@@ -0,0 +1,43 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import unittest
+
+import yaml
+
+from yamllint.rules.common import get_line_indent
+
+
+class CommonTestCase(unittest.TestCase):
+ def test_get_line_indent(self):
+ tokens = list(yaml.scan('a: 1\n'
+ 'b:\n'
+ ' - c: [2, 3, {d: 4}]\n'))
+
+ self.assertEqual(tokens[3].value, 'a')
+ self.assertEqual(tokens[5].value, '1')
+ self.assertEqual(tokens[7].value, 'b')
+ self.assertEqual(tokens[13].value, 'c')
+ self.assertEqual(tokens[16].value, '2')
+ self.assertEqual(tokens[18].value, '3')
+ self.assertEqual(tokens[22].value, 'd')
+ self.assertEqual(tokens[24].value, '4')
+
+ for i in (3, 5):
+ self.assertEqual(get_line_indent(tokens[i]), 0)
+ for i in (7,):
+ self.assertEqual(get_line_indent(tokens[i]), 0)
+ for i in (13, 16, 18, 22, 24):
+ self.assertEqual(get_line_indent(tokens[i]), 2)
diff --git a/tests/rules/test_document_end.py b/tests/rules/test_document_end.py
new file mode 100644
index 0000000..8340c6f
--- /dev/null
+++ b/tests/rules/test_document_end.py
@@ -0,0 +1,92 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class DocumentEndTestCase(RuleTestCase):
+ rule_id = 'document-end'
+
+ def test_disabled(self):
+ conf = 'document-end: disable'
+ self.check('---\n'
+ 'with:\n'
+ ' document: end\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'without:\n'
+ ' document: end\n', conf)
+
+ def test_required(self):
+ conf = 'document-end: {present: true}'
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check('---\n'
+ 'with:\n'
+ ' document: end\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'without:\n'
+ ' document: end\n', conf, problem=(3, 1))
+
+ def test_forbidden(self):
+ conf = 'document-end: {present: false}'
+ self.check('---\n'
+ 'with:\n'
+ ' document: end\n'
+ '...\n', conf, problem=(4, 1))
+ self.check('---\n'
+ 'without:\n'
+ ' document: end\n', conf)
+
+ def test_multiple_documents(self):
+ conf = ('document-end: {present: true}\n'
+ 'document-start: disable\n')
+ self.check('---\n'
+ 'first: document\n'
+ '...\n'
+ '---\n'
+ 'second: document\n'
+ '...\n'
+ '---\n'
+ 'third: document\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'first: document\n'
+ '...\n'
+ '---\n'
+ 'second: document\n'
+ '---\n'
+ 'third: document\n'
+ '...\n', conf, problem=(6, 1))
+
+ def test_directives(self):
+ conf = 'document-end: {present: true}'
+ self.check('%YAML 1.2\n'
+ '---\n'
+ 'document: end\n'
+ '...\n', conf)
+ self.check('%YAML 1.2\n'
+ '%TAG ! tag:clarkevans.com,2002:\n'
+ '---\n'
+ 'document: end\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'first: document\n'
+ '...\n'
+ '%YAML 1.2\n'
+ '---\n'
+ 'second: document\n'
+ '...\n', conf)
diff --git a/tests/rules/test_document_start.py b/tests/rules/test_document_start.py
new file mode 100644
index 0000000..ee2e9d3
--- /dev/null
+++ b/tests/rules/test_document_start.py
@@ -0,0 +1,103 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class DocumentStartTestCase(RuleTestCase):
+ rule_id = 'document-start'
+
+ def test_disabled(self):
+ conf = 'document-start: disable'
+ self.check('', conf)
+ self.check('key: val\n', conf)
+ self.check('---\n'
+ 'key: val\n', conf)
+
+ def test_required(self):
+ conf = ('document-start: {present: true}\n'
+ 'empty-lines: disable\n')
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check('key: val\n', conf, problem=(1, 1))
+ self.check('\n'
+ '\n'
+ 'key: val\n', conf, problem=(3, 1))
+ self.check('---\n'
+ 'key: val\n', conf)
+ self.check('\n'
+ '\n'
+ '---\n'
+ 'key: val\n', conf)
+
+ def test_forbidden(self):
+ conf = ('document-start: {present: false}\n'
+ 'empty-lines: disable\n')
+ self.check('', conf)
+ self.check('key: val\n', conf)
+ self.check('\n'
+ '\n'
+ 'key: val\n', conf)
+ self.check('---\n'
+ 'key: val\n', conf, problem=(1, 1))
+ self.check('\n'
+ '\n'
+ '---\n'
+ 'key: val\n', conf, problem=(3, 1))
+ self.check('first: document\n'
+ '---\n'
+ 'key: val\n', conf, problem=(2, 1))
+
+ def test_multiple_documents(self):
+ conf = 'document-start: {present: true}'
+ self.check('---\n'
+ 'first: document\n'
+ '...\n'
+ '---\n'
+ 'second: document\n'
+ '...\n'
+ '---\n'
+ 'third: document\n', conf)
+ self.check('---\n'
+ 'first: document\n'
+ '---\n'
+ 'second: document\n'
+ '---\n'
+ 'third: document\n', conf)
+ self.check('---\n'
+ 'first: document\n'
+ '...\n'
+ 'second: document\n'
+ '---\n'
+ 'third: document\n', conf, problem=(4, 1, 'syntax'))
+
+ def test_directives(self):
+ conf = 'document-start: {present: true}'
+ self.check('%YAML 1.2\n'
+ '---\n'
+ 'doc: ument\n'
+ '...\n', conf)
+ self.check('%YAML 1.2\n'
+ '%TAG ! tag:clarkevans.com,2002:\n'
+ '---\n'
+ 'doc: ument\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'doc: 1\n'
+ '...\n'
+ '%YAML 1.2\n'
+ '---\n'
+ 'doc: 2\n'
+ '...\n', conf)
diff --git a/tests/rules/test_empty_lines.py b/tests/rules/test_empty_lines.py
new file mode 100644
index 0000000..fbecbcd
--- /dev/null
+++ b/tests/rules/test_empty_lines.py
@@ -0,0 +1,98 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class EmptyLinesTestCase(RuleTestCase):
+ rule_id = 'empty-lines'
+
+ def test_disabled(self):
+ conf = ('empty-lines: disable\n'
+ 'new-line-at-end-of-file: disable\n'
+ 'document-start: disable\n')
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check('\n\n', conf)
+ self.check('\n\n\n\n\n\n\n\n\n', conf)
+ self.check('some text\n\n\n\n\n\n\n\n\n', conf)
+ self.check('\n\n\n\n\n\n\n\n\nsome text', conf)
+ self.check('\n\n\nsome text\n\n\n', conf)
+
+ def test_empty_document(self):
+ conf = ('empty-lines: {max: 0, max-start: 0, max-end: 0}\n'
+ 'new-line-at-end-of-file: disable\n'
+ 'document-start: disable\n')
+ self.check('', conf)
+ self.check('\n', conf)
+
+ def test_0_empty_lines(self):
+ conf = ('empty-lines: {max: 0, max-start: 0, max-end: 0}\n'
+ 'new-line-at-end-of-file: disable\n')
+ self.check('---\n', conf)
+ self.check('---\ntext\n\ntext', conf, problem=(3, 1))
+ self.check('---\ntext\n\ntext\n', conf, problem=(3, 1))
+
+ def test_10_empty_lines(self):
+ conf = 'empty-lines: {max: 10, max-start: 0, max-end: 0}'
+ self.check('---\nintro\n\n\n\n\n\n\n\n\n\n\nconclusion\n', conf)
+ self.check('---\nintro\n\n\n\n\n\n\n\n\n\n\n\nconclusion\n', conf,
+ problem=(13, 1))
+
+ def test_spaces(self):
+ conf = ('empty-lines: {max: 1, max-start: 0, max-end: 0}\n'
+ 'trailing-spaces: disable\n')
+ self.check('---\nintro\n\n \n\nconclusion\n', conf)
+ self.check('---\nintro\n\n \n\n\nconclusion\n', conf, problem=(6, 1))
+
+ def test_empty_lines_at_start(self):
+ conf = ('empty-lines: {max: 2, max-start: 4, max-end: 0}\n'
+ 'document-start: disable\n')
+ self.check('\n\n\n\nnon empty\n', conf)
+ self.check('\n\n\n\n\nnon empty\n', conf, problem=(5, 1))
+
+ conf = ('empty-lines: {max: 2, max-start: 0, max-end: 0}\n'
+ 'document-start: disable\n')
+ self.check('non empty\n', conf)
+ self.check('\nnon empty\n', conf, problem=(1, 1))
+
+ def test_empty_lines_at_end(self):
+ conf = ('empty-lines: {max: 2, max-start: 0, max-end: 4}\n'
+ 'document-start: disable\n')
+ self.check('non empty\n\n\n\n\n', conf)
+ self.check('non empty\n\n\n\n\n\n', conf, problem=(6, 1))
+ conf = ('empty-lines: {max: 2, max-start: 0, max-end: 0}\n'
+ '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_empty_values.py b/tests/rules/test_empty_values.py
new file mode 100644
index 0000000..653f218
--- /dev/null
+++ b/tests/rules/test_empty_values.py
@@ -0,0 +1,368 @@
+# Copyright (C) 2017 Greg Dubicki
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class EmptyValuesTestCase(RuleTestCase):
+ rule_id = 'empty-values'
+
+ def test_disabled(self):
+ conf = ('empty-values: disable\n'
+ 'braces: disable\n'
+ 'commas: disable\n')
+ self.check('---\n'
+ 'foo:\n', conf)
+ self.check('---\n'
+ 'foo:\n'
+ ' bar:\n', conf)
+ self.check('---\n'
+ '{a:}\n', conf)
+ self.check('---\n'
+ 'foo: {a:}\n', conf)
+ self.check('---\n'
+ '- {a:}\n'
+ '- {a:, b: 2}\n'
+ '- {a: 1, b:}\n'
+ '- {a: 1, b: , }\n', conf)
+ self.check('---\n'
+ '{a: {b: , c: {d: 4, e:}}, f:}\n', conf)
+
+ def test_in_block_mappings_disabled(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'foo:\n', conf)
+ self.check('---\n'
+ 'foo:\n'
+ 'bar: aaa\n', conf)
+
+ def test_in_block_mappings_single_line(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'implicitly-null:\n', conf, problem1=(2, 17))
+ self.check('---\n'
+ 'implicitly-null:with-colons:in-key:\n', conf,
+ problem1=(2, 36))
+ self.check('---\n'
+ 'implicitly-null:with-colons:in-key2:\n', conf,
+ problem1=(2, 37))
+
+ def test_in_block_mappings_all_lines(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'foo:\n'
+ 'bar:\n'
+ 'foobar:\n', conf, problem1=(2, 5),
+ problem2=(3, 5), problem3=(4, 8))
+
+ def test_in_block_mappings_explicit_end_of_document(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'foo:\n'
+ '...\n', conf, problem1=(2, 5))
+
+ def test_in_block_mappings_not_end_of_document(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'foo:\n'
+ 'bar:\n'
+ ' aaa\n', conf, problem1=(2, 5))
+
+ def test_in_block_mappings_different_level(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'foo:\n'
+ ' bar:\n'
+ 'aaa: bbb\n', conf, problem1=(3, 6))
+
+ def test_in_block_mappings_empty_flow_mapping(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n'
+ 'braces: disable\n'
+ 'commas: disable\n')
+ self.check('---\n'
+ 'foo: {a:}\n', conf)
+ self.check('---\n'
+ '- {a:, b: 2}\n'
+ '- {a: 1, b:}\n'
+ '- {a: 1, b: , }\n', conf)
+
+ def test_in_block_mappings_empty_block_sequence(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'foo:\n'
+ ' -\n', conf)
+
+ def test_in_block_mappings_not_empty_or_explicit_null(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'foo:\n'
+ ' bar:\n'
+ ' aaa\n', conf)
+ self.check('---\n'
+ 'explicitly-null: null\n', conf)
+ self.check('---\n'
+ 'explicitly-null:with-colons:in-key: null\n', conf)
+ self.check('---\n'
+ 'false-null: nulL\n', conf)
+ self.check('---\n'
+ 'empty-string: \'\'\n', conf)
+ self.check('---\n'
+ 'nullable-boolean: false\n', conf)
+ self.check('---\n'
+ 'nullable-int: 0\n', conf)
+ self.check('---\n'
+ 'First occurrence: &anchor Foo\n'
+ 'Second occurrence: *anchor\n', conf)
+
+ def test_in_block_mappings_various_explicit_null(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'null-alias: ~\n', conf)
+ self.check('---\n'
+ 'null-key1: {?: val}\n', conf)
+ self.check('---\n'
+ 'null-key2: {? !!null "": val}\n', conf)
+
+ def test_in_block_mappings_comments(self):
+ conf = ('empty-values: {forbid-in-block-mappings: true,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n'
+ 'comments: disable\n')
+ self.check('---\n'
+ 'empty: # comment\n'
+ 'foo:\n'
+ ' bar: # comment\n', conf,
+ problem1=(2, 7),
+ problem2=(4, 7))
+
+ def test_in_flow_mappings_disabled(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n'
+ 'braces: disable\n'
+ 'commas: disable\n')
+ self.check('---\n'
+ '{a:}\n', conf)
+ self.check('---\n'
+ 'foo: {a:}\n', conf)
+ self.check('---\n'
+ '- {a:}\n'
+ '- {a:, b: 2}\n'
+ '- {a: 1, b:}\n'
+ '- {a: 1, b: , }\n', conf)
+ self.check('---\n'
+ '{a: {b: , c: {d: 4, e:}}, f:}\n', conf)
+
+ def test_in_flow_mappings_single_line(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: true,\n'
+ ' forbid-in-block-sequences: false}\n'
+ 'braces: disable\n'
+ 'commas: disable\n')
+ self.check('---\n'
+ '{a:}\n', conf,
+ problem=(2, 4))
+ self.check('---\n'
+ 'foo: {a:}\n', conf,
+ problem=(2, 9))
+ self.check('---\n'
+ '- {a:}\n'
+ '- {a:, b: 2}\n'
+ '- {a: 1, b:}\n'
+ '- {a: 1, b: , }\n', conf,
+ problem1=(2, 6),
+ problem2=(3, 6),
+ problem3=(4, 12),
+ problem4=(5, 12))
+ self.check('---\n'
+ '{a: {b: , c: {d: 4, e:}}, f:}\n', conf,
+ problem1=(2, 8),
+ problem2=(2, 23),
+ problem3=(2, 29))
+
+ def test_in_flow_mappings_multi_line(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: true,\n'
+ ' forbid-in-block-sequences: false}\n'
+ 'braces: disable\n'
+ 'commas: disable\n')
+ self.check('---\n'
+ 'foo: {\n'
+ ' a:\n'
+ '}\n', conf,
+ problem=(3, 5))
+ self.check('---\n'
+ '{\n'
+ ' a: {\n'
+ ' b: ,\n'
+ ' c: {\n'
+ ' d: 4,\n'
+ ' e:\n'
+ ' }\n'
+ ' },\n'
+ ' f:\n'
+ '}\n', conf,
+ problem1=(4, 7),
+ problem2=(7, 9),
+ problem3=(10, 5))
+
+ def test_in_flow_mappings_various_explicit_null(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: true,\n'
+ ' forbid-in-block-sequences: false}\n'
+ 'braces: disable\n'
+ 'commas: disable\n')
+ self.check('---\n'
+ '{explicit-null: null}\n', conf)
+ self.check('---\n'
+ '{null-alias: ~}\n', conf)
+ self.check('---\n'
+ 'null-key1: {?: val}\n', conf)
+ self.check('---\n'
+ 'null-key2: {? !!null "": val}\n', conf)
+
+ def test_in_flow_mappings_comments(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: true,\n'
+ ' forbid-in-block-sequences: false}\n'
+ 'braces: disable\n'
+ 'commas: disable\n'
+ 'comments: disable\n')
+ self.check('---\n'
+ '{\n'
+ ' a: {\n'
+ ' b: , # comment\n'
+ ' c: {\n'
+ ' d: 4, # comment\n'
+ ' e: # comment\n'
+ ' }\n'
+ ' },\n'
+ ' f: # comment\n'
+ '}\n', conf,
+ problem1=(4, 7),
+ problem2=(7, 9),
+ problem3=(10, 5))
+
+ def test_in_block_sequences_disabled(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: false}\n')
+ self.check('---\n'
+ 'foo:\n'
+ ' - bar\n'
+ ' -\n', conf)
+ self.check('---\n'
+ 'foo:\n'
+ ' -\n', conf)
+
+ def test_in_block_sequences_primative_item(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: true}\n')
+ self.check('---\n'
+ 'foo:\n'
+ ' -\n', conf,
+ problem=(3, 4))
+ self.check('---\n'
+ 'foo:\n'
+ ' - bar\n'
+ ' -\n', conf,
+ problem=(4, 4))
+ self.check('---\n'
+ 'foo:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' -\n', conf,
+ problem=(5, 4))
+ self.check('---\n'
+ 'foo:\n'
+ ' - true\n', conf)
+
+ def test_in_block_sequences_complex_objects(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: true}\n')
+ self.check('---\n'
+ 'foo:\n'
+ ' - a: 1\n', conf)
+ self.check('---\n'
+ 'foo:\n'
+ ' - a: 1\n'
+ ' -\n', conf,
+ problem=(4, 4))
+ self.check('---\n'
+ 'foo:\n'
+ ' - a: 1\n'
+ ' b: 2\n'
+ ' -\n', conf,
+ problem=(5, 4))
+ self.check('---\n'
+ 'foo:\n'
+ ' - a: 1\n'
+ ' - b: 2\n'
+ ' -\n', conf,
+ problem=(5, 4))
+ self.check('---\n'
+ 'foo:\n'
+ ' - - a\n'
+ ' - b: 2\n'
+ ' -\n', conf,
+ problem=(5, 6))
+ self.check('---\n'
+ 'foo:\n'
+ ' - - a\n'
+ ' - b: 2\n'
+ ' -\n', conf,
+ problem=(5, 4))
+
+ def test_in_block_sequences_various_explicit_null(self):
+ conf = ('empty-values: {forbid-in-block-mappings: false,\n'
+ ' forbid-in-flow-mappings: false,\n'
+ ' forbid-in-block-sequences: true}\n')
+ self.check('---\n'
+ 'foo:\n'
+ ' - null\n', conf)
+ self.check('---\n'
+ '- null\n', conf)
+ self.check('---\n'
+ 'foo:\n'
+ ' - bar: null\n'
+ ' - null\n', conf)
+ self.check('---\n'
+ '- null\n'
+ '- null\n', conf)
+ self.check('---\n'
+ '- - null\n'
+ ' - null\n', conf)
diff --git a/tests/rules/test_float_values.py b/tests/rules/test_float_values.py
new file mode 100644
index 0000000..8aa980c
--- /dev/null
+++ b/tests/rules/test_float_values.py
@@ -0,0 +1,128 @@
+# Copyright (C) 2022 the yamllint contributors
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class FloatValuesTestCase(RuleTestCase):
+ rule_id = 'float-values'
+
+ def test_disabled(self):
+ conf = 'float-values: disable\n'
+ self.check('---\n'
+ '- 0.0\n'
+ '- .NaN\n'
+ '- .INF\n'
+ '- .1\n'
+ '- 10e-6\n',
+ conf)
+
+ def test_numeral_before_decimal(self):
+ conf = (
+ 'float-values:\n'
+ ' require-numeral-before-decimal: true\n'
+ ' forbid-scientific-notation: false\n'
+ ' forbid-nan: false\n'
+ ' forbid-inf: false\n')
+ self.check('---\n'
+ '- 0.0\n'
+ '- .1\n'
+ '- \'.1\'\n'
+ '- string.1\n'
+ '- .1string\n'
+ '- !custom_tag .2\n'
+ '- &angle1 0.0\n'
+ '- *angle1\n'
+ '- &angle2 .3\n'
+ '- *angle2\n',
+ conf,
+ problem1=(3, 3),
+ problem2=(10, 11))
+
+ def test_scientific_notation(self):
+ conf = (
+ 'float-values:\n'
+ ' require-numeral-before-decimal: false\n'
+ ' forbid-scientific-notation: true\n'
+ ' forbid-nan: false\n'
+ ' forbid-inf: false\n')
+ self.check('---\n'
+ '- 10e6\n'
+ '- 10e-6\n'
+ '- 0.00001\n'
+ '- \'10e-6\'\n'
+ '- string10e-6\n'
+ '- 10e-6string\n'
+ '- !custom_tag 10e-6\n'
+ '- &angle1 0.000001\n'
+ '- *angle1\n'
+ '- &angle2 10e-6\n'
+ '- *angle2\n'
+ '- &angle3 10e6\n'
+ '- *angle3\n',
+ conf,
+ problem1=(2, 3),
+ problem2=(3, 3),
+ problem3=(11, 11),
+ problem4=(13, 11))
+
+ def test_nan(self):
+ conf = (
+ 'float-values:\n'
+ ' require-numeral-before-decimal: false\n'
+ ' forbid-scientific-notation: false\n'
+ ' forbid-nan: true\n'
+ ' forbid-inf: false\n')
+ self.check('---\n'
+ '- .NaN\n'
+ '- .NAN\n'
+ '- \'.NaN\'\n'
+ '- a.NaN\n'
+ '- .NaNa\n'
+ '- !custom_tag .NaN\n'
+ '- &angle .nan\n'
+ '- *angle\n',
+ conf,
+ problem1=(2, 3),
+ problem2=(3, 3),
+ problem3=(8, 10))
+
+ def test_inf(self):
+ conf = (
+ 'float-values:\n'
+ ' require-numeral-before-decimal: false\n'
+ ' forbid-scientific-notation: false\n'
+ ' forbid-nan: false\n'
+ ' forbid-inf: true\n')
+ self.check('---\n'
+ '- .inf\n'
+ '- .INF\n'
+ '- -.inf\n'
+ '- -.INF\n'
+ '- \'.inf\'\n'
+ '- ∞.infinity\n'
+ '- .infinity∞\n'
+ '- !custom_tag .inf\n'
+ '- &angle .inf\n'
+ '- *angle\n'
+ '- &angle -.inf\n'
+ '- *angle\n',
+ conf,
+ problem1=(2, 3),
+ problem2=(3, 3),
+ problem3=(4, 3),
+ problem4=(5, 3),
+ problem5=(10, 10),
+ problem6=(12, 10))
diff --git a/tests/rules/test_hyphens.py b/tests/rules/test_hyphens.py
new file mode 100644
index 0000000..a0ec577
--- /dev/null
+++ b/tests/rules/test_hyphens.py
@@ -0,0 +1,105 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class HyphenTestCase(RuleTestCase):
+ rule_id = 'hyphens'
+
+ def test_disabled(self):
+ conf = 'hyphens: disable'
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf)
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf)
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf)
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' subobject:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' subobject:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf)
+
+ def test_enabled(self):
+ conf = 'hyphens: {max-spaces-after: 1}'
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf)
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf, problem=(3, 3))
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf, problem1=(2, 3), problem2=(3, 3))
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf, problem=(2, 3))
+ self.check('---\n'
+ 'object:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf, problem=(4, 5))
+ self.check('---\n'
+ 'object:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf, problem1=(3, 5), problem2=(4, 5))
+ self.check('---\n'
+ 'object:\n'
+ ' subobject:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf, problem=(5, 7))
+ self.check('---\n'
+ 'object:\n'
+ ' subobject:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf, problem1=(4, 7), problem2=(5, 7))
+
+ def test_max_3(self):
+ conf = 'hyphens: {max-spaces-after: 3}'
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf)
+ self.check('---\n'
+ '- elem1\n'
+ '- elem2\n', conf, problem=(2, 5))
+ self.check('---\n'
+ 'a:\n'
+ ' b:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf)
+ self.check('---\n'
+ 'a:\n'
+ ' b:\n'
+ ' - elem1\n'
+ ' - elem2\n', conf, problem1=(4, 9), problem2=(5, 9))
diff --git a/tests/rules/test_indentation.py b/tests/rules/test_indentation.py
new file mode 100644
index 0000000..1c6eddb
--- /dev/null
+++ b/tests/rules/test_indentation.py
@@ -0,0 +1,2160 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+from yamllint.parser import token_or_comment_generator, Comment
+from yamllint.rules.indentation import check
+
+
+class IndentationStackTestCase(RuleTestCase):
+ # This test suite checks that the "indentation stack" built by the
+ # indentation rule is valid. It is important, since everything else in the
+ # rule relies on this stack.
+
+ maxDiff = None
+
+ def format_stack(self, stack):
+ """Transform the stack at a given moment into a printable string like:
+
+ B_MAP:0 KEY:0 VAL:5
+ """
+ return ' '.join(map(str, stack[1:]))
+
+ def full_stack(self, source):
+ conf = {'spaces': 2, 'indent-sequences': True,
+ 'check-multi-line-strings': False}
+ context = {}
+ output = ''
+ for elem in [t for t in token_or_comment_generator(source)
+ if not isinstance(t, Comment)]:
+ list(check(conf, elem.curr, elem.prev, elem.next, elem.nextnext,
+ context))
+
+ token_type = (elem.curr.__class__.__name__
+ .replace('Token', '')
+ .replace('Block', 'B').replace('Flow', 'F')
+ .replace('Sequence', 'Seq')
+ .replace('Mapping', 'Map'))
+ if token_type in ('StreamStart', 'StreamEnd'):
+ continue
+ output += '{:>9} {}\n'.format(token_type,
+ self.format_stack(context['stack']))
+ return output
+
+ def test_simple_mapping(self):
+ self.assertMultiLineEqual(
+ self.full_stack('key: val\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:5\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack(' key: val\n'),
+ 'BMapStart B_MAP:5\n'
+ ' Key B_MAP:5 KEY:5\n'
+ ' Scalar B_MAP:5 KEY:5\n'
+ ' Value B_MAP:5 KEY:5 VAL:10\n'
+ ' Scalar B_MAP:5\n'
+ ' BEnd \n')
+
+ def test_simple_sequence(self):
+ self.assertMultiLineEqual(
+ self.full_stack('- 1\n'
+ '- 2\n'
+ '- 3\n'),
+ 'BSeqStart B_SEQ:0\n'
+ ' BEntry B_SEQ:0 B_ENT:2\n'
+ ' Scalar B_SEQ:0\n'
+ ' BEntry B_SEQ:0 B_ENT:2\n'
+ ' Scalar B_SEQ:0\n'
+ ' BEntry B_SEQ:0 B_ENT:2\n'
+ ' Scalar B_SEQ:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('key:\n'
+ ' - 1\n'
+ ' - 2\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ 'BSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:2 B_ENT:4\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:2 B_ENT:4\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
+ ' BEnd B_MAP:0\n'
+ ' BEnd \n')
+
+ def test_non_indented_sequences(self):
+ # There seems to be a bug in pyyaml: depending on the indentation, a
+ # sequence does not produce the same tokens. More precisely, the
+ # following YAML:
+ # usr:
+ # - lib
+ # produces a BlockSequenceStartToken and a BlockEndToken around the
+ # "lib" sequence, whereas the following:
+ # usr:
+ # - lib
+ # does not (both two tokens are omitted).
+ # So, yamllint must create fake 'B_SEQ'. This test makes sure it does.
+
+ self.assertMultiLineEqual(
+ self.full_stack('usr:\n'
+ ' - lib\n'
+ 'var: cache\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ 'BSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:2 B_ENT:4\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
+ ' BEnd B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:5\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('usr:\n'
+ '- lib\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ # missing BSeqStart here
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ ' Scalar B_MAP:0\n'
+ # missing BEnd here
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('usr:\n'
+ '- lib\n'
+ 'var: cache\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ # missing BSeqStart here
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ ' Scalar B_MAP:0\n'
+ # missing BEnd here
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:5\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('usr:\n'
+ '- []\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ # missing BSeqStart here
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ 'FSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 F_SEQ:3\n'
+ ' FSeqEnd B_MAP:0\n'
+ # missing BEnd here
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('usr:\n'
+ '- k:\n'
+ ' v\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ # missing BSeqStart here
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ 'BMapStart B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2\n'
+ ' Key B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n'
+ ' Value B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2 VAL:4\n' # noqa
+ ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2\n'
+ ' BEnd B_MAP:0\n'
+ # missing BEnd here
+ ' BEnd \n')
+
+ def test_flows(self):
+ self.assertMultiLineEqual(
+ self.full_stack('usr: [\n'
+ ' {k:\n'
+ ' v}\n'
+ ' ]\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:5\n'
+ 'FSeqStart B_MAP:0 KEY:0 VAL:5 F_SEQ:2\n'
+ 'FMapStart B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3\n'
+ ' Key B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3 KEY:3\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3 KEY:3\n'
+ ' Value B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3 KEY:3 VAL:5\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3\n'
+ ' FMapEnd B_MAP:0 KEY:0 VAL:5 F_SEQ:2\n'
+ ' FSeqEnd B_MAP:0\n'
+ ' BEnd \n')
+
+ def test_anchors(self):
+ self.assertMultiLineEqual(
+ self.full_stack('key: &anchor value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:5\n'
+ ' Anchor B_MAP:0 KEY:0 VAL:5\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('key: &anchor\n'
+ ' value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ ' Anchor B_MAP:0 KEY:0 VAL:2\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('- &anchor value\n'),
+ 'BSeqStart B_SEQ:0\n'
+ ' BEntry B_SEQ:0 B_ENT:2\n'
+ ' Anchor B_SEQ:0 B_ENT:2\n'
+ ' Scalar B_SEQ:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('- &anchor\n'
+ ' value\n'),
+ 'BSeqStart B_SEQ:0\n'
+ ' BEntry B_SEQ:0 B_ENT:2\n'
+ ' Anchor B_SEQ:0 B_ENT:2\n'
+ ' Scalar B_SEQ:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('- &anchor\n'
+ ' - 1\n'
+ ' - 2\n'),
+ 'BSeqStart B_SEQ:0\n'
+ ' BEntry B_SEQ:0 B_ENT:2\n'
+ ' Anchor B_SEQ:0 B_ENT:2\n'
+ 'BSeqStart B_SEQ:0 B_ENT:2 B_SEQ:2\n'
+ ' BEntry B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
+ ' Scalar B_SEQ:0 B_ENT:2 B_SEQ:2\n'
+ ' BEntry B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
+ ' Scalar B_SEQ:0 B_ENT:2 B_SEQ:2\n'
+ ' BEnd B_SEQ:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('&anchor key:\n'
+ ' value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Anchor B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('pre:\n'
+ ' &anchor1 0\n'
+ '&anchor2 key:\n'
+ ' value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ ' Anchor B_MAP:0 KEY:0 VAL:2\n'
+ ' Scalar B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Anchor B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('sequence: &anchor\n'
+ '- entry\n'
+ '- &anchor\n'
+ ' - nested\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ ' Anchor B_MAP:0 KEY:0 VAL:2\n'
+ # missing BSeqStart here
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0\n'
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ ' Anchor B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ 'BSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2\n'
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2\n'
+ ' BEnd B_MAP:0\n'
+ # missing BEnd here
+ ' BEnd \n')
+
+ def test_tags(self):
+ self.assertMultiLineEqual(
+ self.full_stack('key: !!tag value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:5\n'
+ ' Tag B_MAP:0 KEY:0 VAL:5\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('- !!map # Block collection\n'
+ ' foo : bar\n'),
+ 'BSeqStart B_SEQ:0\n'
+ ' BEntry B_SEQ:0 B_ENT:2\n'
+ ' Tag B_SEQ:0 B_ENT:2\n'
+ 'BMapStart B_SEQ:0 B_ENT:2 B_MAP:2\n'
+ ' Key B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n'
+ ' Scalar B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n'
+ ' Value B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2 VAL:8\n'
+ ' Scalar B_SEQ:0 B_ENT:2 B_MAP:2\n'
+ ' BEnd B_SEQ:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('- !!seq\n'
+ ' - nested item\n'),
+ 'BSeqStart B_SEQ:0\n'
+ ' BEntry B_SEQ:0 B_ENT:2\n'
+ ' Tag B_SEQ:0 B_ENT:2\n'
+ 'BSeqStart B_SEQ:0 B_ENT:2 B_SEQ:2\n'
+ ' BEntry B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
+ ' Scalar B_SEQ:0 B_ENT:2 B_SEQ:2\n'
+ ' BEnd B_SEQ:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('sequence: !!seq\n'
+ '- entry\n'
+ '- !!seq\n'
+ ' - nested\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ ' Scalar B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:2\n'
+ ' Tag B_MAP:0 KEY:0 VAL:2\n'
+ # missing BSeqStart here
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0\n'
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ ' Tag B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
+ 'BSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2\n'
+ ' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
+ ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2\n'
+ ' BEnd B_MAP:0\n'
+ # missing BEnd here
+ ' BEnd \n')
+
+ def test_flows_imbrication(self):
+ self.assertMultiLineEqual(
+ self.full_stack('[[val]]\n'),
+ 'FSeqStart F_SEQ:1\n'
+ 'FSeqStart F_SEQ:1 F_SEQ:2\n'
+ ' Scalar F_SEQ:1 F_SEQ:2\n'
+ ' FSeqEnd F_SEQ:1\n'
+ ' FSeqEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('[[val], [val2]]\n'),
+ 'FSeqStart F_SEQ:1\n'
+ 'FSeqStart F_SEQ:1 F_SEQ:2\n'
+ ' Scalar F_SEQ:1 F_SEQ:2\n'
+ ' FSeqEnd F_SEQ:1\n'
+ ' FEntry F_SEQ:1\n'
+ 'FSeqStart F_SEQ:1 F_SEQ:9\n'
+ ' Scalar F_SEQ:1 F_SEQ:9\n'
+ ' FSeqEnd F_SEQ:1\n'
+ ' FSeqEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('{{key}}\n'),
+ 'FMapStart F_MAP:1\n'
+ 'FMapStart F_MAP:1 F_MAP:2\n'
+ ' Scalar F_MAP:1 F_MAP:2\n'
+ ' FMapEnd F_MAP:1\n'
+ ' FMapEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('[key]: value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ 'FSeqStart B_MAP:0 KEY:0 F_SEQ:1\n'
+ ' Scalar B_MAP:0 KEY:0 F_SEQ:1\n'
+ ' FSeqEnd B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:7\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('[[key]]: value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ 'FSeqStart B_MAP:0 KEY:0 F_SEQ:1\n'
+ 'FSeqStart B_MAP:0 KEY:0 F_SEQ:1 F_SEQ:2\n'
+ ' Scalar B_MAP:0 KEY:0 F_SEQ:1 F_SEQ:2\n'
+ ' FSeqEnd B_MAP:0 KEY:0 F_SEQ:1\n'
+ ' FSeqEnd B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:9\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('{key}: value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ 'FMapStart B_MAP:0 KEY:0 F_MAP:1\n'
+ ' Scalar B_MAP:0 KEY:0 F_MAP:1\n'
+ ' FMapEnd B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:7\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('{key: value}: value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ 'FMapStart B_MAP:0 KEY:0 F_MAP:1\n'
+ ' Key B_MAP:0 KEY:0 F_MAP:1 KEY:1\n'
+ ' Scalar B_MAP:0 KEY:0 F_MAP:1 KEY:1\n'
+ ' Value B_MAP:0 KEY:0 F_MAP:1 KEY:1 VAL:6\n'
+ ' Scalar B_MAP:0 KEY:0 F_MAP:1\n'
+ ' FMapEnd B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:14\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('{{key}}: value\n'),
+ 'BMapStart B_MAP:0\n'
+ ' Key B_MAP:0 KEY:0\n'
+ 'FMapStart B_MAP:0 KEY:0 F_MAP:1\n'
+ 'FMapStart B_MAP:0 KEY:0 F_MAP:1 F_MAP:2\n'
+ ' Scalar B_MAP:0 KEY:0 F_MAP:1 F_MAP:2\n'
+ ' FMapEnd B_MAP:0 KEY:0 F_MAP:1\n'
+ ' FMapEnd B_MAP:0 KEY:0\n'
+ ' Value B_MAP:0 KEY:0 VAL:9\n'
+ ' Scalar B_MAP:0\n'
+ ' BEnd \n')
+ self.assertMultiLineEqual(
+ self.full_stack('{{key}: val, {key2}: {val2}}\n'),
+ 'FMapStart F_MAP:1\n'
+ ' Key F_MAP:1 KEY:1\n'
+ 'FMapStart F_MAP:1 KEY:1 F_MAP:2\n'
+ ' Scalar F_MAP:1 KEY:1 F_MAP:2\n'
+ ' FMapEnd F_MAP:1 KEY:1\n'
+ ' Value F_MAP:1 KEY:1 VAL:8\n'
+ ' Scalar F_MAP:1\n'
+ ' FEntry F_MAP:1\n'
+ ' Key F_MAP:1 KEY:1\n'
+ 'FMapStart F_MAP:1 KEY:1 F_MAP:14\n'
+ ' Scalar F_MAP:1 KEY:1 F_MAP:14\n'
+ ' FMapEnd F_MAP:1 KEY:1\n'
+ ' Value F_MAP:1 KEY:1 VAL:21\n'
+ 'FMapStart F_MAP:1 KEY:1 VAL:21 F_MAP:22\n'
+ ' Scalar F_MAP:1 KEY:1 VAL:21 F_MAP:22\n'
+ ' FMapEnd F_MAP:1\n'
+ ' FMapEnd \n')
+
+ self.assertMultiLineEqual(
+ self.full_stack('{[{{[val]}}, [{[key]: val2}]]}\n'),
+ 'FMapStart F_MAP:1\n'
+ 'FSeqStart F_MAP:1 F_SEQ:2\n'
+ 'FMapStart F_MAP:1 F_SEQ:2 F_MAP:3\n'
+ 'FMapStart F_MAP:1 F_SEQ:2 F_MAP:3 F_MAP:4\n'
+ 'FSeqStart F_MAP:1 F_SEQ:2 F_MAP:3 F_MAP:4 F_SEQ:5\n'
+ ' Scalar F_MAP:1 F_SEQ:2 F_MAP:3 F_MAP:4 F_SEQ:5\n'
+ ' FSeqEnd F_MAP:1 F_SEQ:2 F_MAP:3 F_MAP:4\n'
+ ' FMapEnd F_MAP:1 F_SEQ:2 F_MAP:3\n'
+ ' FMapEnd F_MAP:1 F_SEQ:2\n'
+ ' FEntry F_MAP:1 F_SEQ:2\n'
+ 'FSeqStart F_MAP:1 F_SEQ:2 F_SEQ:14\n'
+ 'FMapStart F_MAP:1 F_SEQ:2 F_SEQ:14 F_MAP:15\n'
+ ' Key F_MAP:1 F_SEQ:2 F_SEQ:14 F_MAP:15 KEY:15\n'
+ 'FSeqStart F_MAP:1 F_SEQ:2 F_SEQ:14 F_MAP:15 KEY:15 F_SEQ:16\n'
+ ' Scalar F_MAP:1 F_SEQ:2 F_SEQ:14 F_MAP:15 KEY:15 F_SEQ:16\n'
+ ' FSeqEnd F_MAP:1 F_SEQ:2 F_SEQ:14 F_MAP:15 KEY:15\n'
+ ' Value F_MAP:1 F_SEQ:2 F_SEQ:14 F_MAP:15 KEY:15 VAL:22\n'
+ ' Scalar F_MAP:1 F_SEQ:2 F_SEQ:14 F_MAP:15\n'
+ ' FMapEnd F_MAP:1 F_SEQ:2 F_SEQ:14\n'
+ ' FSeqEnd F_MAP:1 F_SEQ:2\n'
+ ' FSeqEnd F_MAP:1\n'
+ ' FMapEnd \n')
+
+
+class IndentationTestCase(RuleTestCase):
+ rule_id = 'indentation'
+
+ def test_disabled(self):
+ conf = 'indentation: disable'
+ self.check('---\n'
+ 'object:\n'
+ ' k1: v1\n'
+ 'obj2:\n'
+ ' k2:\n'
+ ' - 8\n'
+ ' k3:\n'
+ ' val\n'
+ '...\n', conf)
+ self.check('---\n'
+ ' o:\n'
+ ' k1: v1\n'
+ ' p:\n'
+ ' k3:\n'
+ ' val\n'
+ '...\n', conf)
+ self.check('---\n'
+ ' - o:\n'
+ ' k1: v1\n'
+ ' - p: kdjf\n'
+ ' - q:\n'
+ ' k3:\n'
+ ' - val\n'
+ '...\n', conf)
+
+ def test_one_space(self):
+ conf = 'indentation: {spaces: 1, indent-sequences: false}'
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ ' - name: Linux\n'
+ ' date: 1991\n'
+ '...\n', conf)
+ conf = 'indentation: {spaces: 1, indent-sequences: true}'
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ ' - name: Linux\n'
+ ' date: 1991\n'
+ '...\n', conf)
+
+ def test_two_spaces(self):
+ conf = 'indentation: {spaces: 2, indent-sequences: false}'
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' 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'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ ' - name: Linux\n'
+ ' date: 1991\n'
+ '...\n', conf)
+
+ def test_three_spaces(self):
+ conf = 'indentation: {spaces: 3, indent-sequences: false}'
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ ' - name: Linux\n'
+ ' date: 1991\n'
+ '...\n', conf)
+ conf = 'indentation: {spaces: 3, indent-sequences: true}'
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ ' - name: Linux\n'
+ ' date: 1991\n'
+ '...\n', conf)
+
+ def test_consistent_spaces(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' indent-sequences: whatever}\n'
+ 'document-start: disable\n')
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ ' - name: Linux\n'
+ ' date: 1991\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ ' - name: Linux\n'
+ ' date: 1991\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ ' - b\n'
+ ' k2: v2\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ ' - name: Linux\n'
+ ' date: 1991\n'
+ '...\n', conf)
+ self.check('first is not indented:\n'
+ ' value is indented\n', conf)
+ self.check('first is not indented:\n'
+ ' value:\n'
+ ' is indented\n', conf)
+ self.check('- first is already indented:\n'
+ ' value is indented too\n', conf)
+ self.check('- first is already indented:\n'
+ ' value:\n'
+ ' is indented too\n', conf)
+ self.check('- first is already indented:\n'
+ ' value:\n'
+ ' is indented too\n', conf, problem=(3, 14))
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem=(7, 5))
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf)
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf)
+
+ def test_consistent_spaces_and_indent_sequences(self):
+ conf = 'indentation: {spaces: consistent, indent-sequences: true}'
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem1=(3, 1))
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem1=(7, 5))
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf, problem1=(7, 1))
+
+ conf = 'indentation: {spaces: consistent, indent-sequences: false}'
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem1=(7, 5))
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem1=(7, 3))
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf, problem1=(3, 3))
+
+ conf = ('indentation: {spaces: consistent,\n'
+ ' indent-sequences: consistent}')
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem1=(7, 5))
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf, problem1=(7, 1))
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf)
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem1=(7, 5))
+
+ conf = 'indentation: {spaces: consistent, indent-sequences: whatever}'
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf)
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf)
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf)
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem1=(7, 5))
+
+ def test_indent_sequences_whatever(self):
+ conf = 'indentation: {spaces: 4, indent-sequences: whatever}'
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf)
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem=(3, 3))
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem=(7, 3))
+ self.check('---\n'
+ 'list:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf, problem=(6, 1, 'syntax'))
+
+ def test_indent_sequences_consistent(self):
+ conf = 'indentation: {spaces: 4, indent-sequences: consistent}'
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list:\n'
+ ' two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf)
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list:\n'
+ ' two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf)
+ self.check('---\n'
+ 'list one:\n'
+ '- 1\n'
+ '- 2\n'
+ '- 3\n'
+ 'list two:\n'
+ ' - a\n'
+ ' - b\n'
+ ' - c\n', conf, problem=(7, 5))
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf, problem=(7, 1))
+ self.check('---\n'
+ 'list one:\n'
+ ' - 1\n'
+ ' - 2\n'
+ ' - 3\n'
+ 'list two:\n'
+ '- a\n'
+ '- b\n'
+ '- c\n', conf, problem1=(3, 2), problem2=(7, 1))
+
+ def test_direct_flows(self):
+ # flow: [ ...
+ # ]
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ 'a: {x: 1,\n'
+ ' y,\n'
+ ' z: 1}\n', conf)
+ self.check('---\n'
+ 'a: {x: 1,\n'
+ ' y,\n'
+ ' z: 1}\n', conf, problem=(3, 4))
+ self.check('---\n'
+ 'a: {x: 1,\n'
+ ' y,\n'
+ ' z: 1}\n', conf, problem=(3, 6))
+ self.check('---\n'
+ 'a: {x: 1,\n'
+ ' y, z: 1}\n', conf, problem=(3, 3))
+ self.check('---\n'
+ 'a: {x: 1,\n'
+ ' y, z: 1\n'
+ '}\n', conf)
+ self.check('---\n'
+ 'a: {x: 1,\n'
+ ' y, z: 1\n'
+ '}\n', conf, problem=(3, 3))
+ self.check('---\n'
+ 'a: [x,\n'
+ ' y,\n'
+ ' z]\n', conf)
+ self.check('---\n'
+ 'a: [x,\n'
+ ' y,\n'
+ ' z]\n', conf, problem=(3, 4))
+ self.check('---\n'
+ 'a: [x,\n'
+ ' y,\n'
+ ' z]\n', conf, problem=(3, 6))
+ self.check('---\n'
+ 'a: [x,\n'
+ ' y, z]\n', conf, problem=(3, 3))
+ self.check('---\n'
+ 'a: [x,\n'
+ ' y, z\n'
+ ']\n', conf)
+ self.check('---\n'
+ 'a: [x,\n'
+ ' y, z\n'
+ ']\n', conf, problem=(3, 3))
+
+ def test_broken_flows(self):
+ # flow: [
+ # ...
+ # ]
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ 'a: {\n'
+ ' x: 1,\n'
+ ' y, z: 1\n'
+ '}\n', conf)
+ self.check('---\n'
+ 'a: {\n'
+ ' x: 1,\n'
+ ' y, z: 1}\n', conf)
+ self.check('---\n'
+ 'a: {\n'
+ ' x: 1,\n'
+ ' y, z: 1\n'
+ '}\n', conf, problem=(4, 3))
+ self.check('---\n'
+ 'a: {\n'
+ ' x: 1,\n'
+ ' y, z: 1\n'
+ ' }\n', conf, problem=(5, 3))
+ self.check('---\n'
+ 'a: [\n'
+ ' x,\n'
+ ' y, z\n'
+ ']\n', conf)
+ self.check('---\n'
+ 'a: [\n'
+ ' x,\n'
+ ' y, z]\n', conf)
+ self.check('---\n'
+ 'a: [\n'
+ ' x,\n'
+ ' y, z\n'
+ ']\n', conf, problem=(4, 3))
+ self.check('---\n'
+ 'a: [\n'
+ ' x,\n'
+ ' y, z\n'
+ ' ]\n', conf, problem=(5, 3))
+ self.check('---\n'
+ 'obj: {\n'
+ ' a: 1,\n'
+ ' b: 2,\n'
+ ' c: 3\n'
+ '}\n', conf, problem1=(4, 4), problem2=(5, 2))
+ self.check('---\n'
+ 'list: [\n'
+ ' 1,\n'
+ ' 2,\n'
+ ' 3\n'
+ ']\n', conf, problem1=(4, 4), problem2=(5, 2))
+ self.check('---\n'
+ 'top:\n'
+ ' rules: [\n'
+ ' 1, 2,\n'
+ ' ]\n', conf)
+ self.check('---\n'
+ 'top:\n'
+ ' rules: [\n'
+ ' 1, 2,\n'
+ ']\n'
+ ' rulez: [\n'
+ ' 1, 2,\n'
+ ' ]\n', conf, problem1=(5, 1), problem2=(8, 5))
+ self.check('---\n'
+ 'top:\n'
+ ' rules:\n'
+ ' here: {\n'
+ ' foo: 1,\n'
+ ' bar: 2\n'
+ ' }\n', conf)
+ self.check('---\n'
+ 'top:\n'
+ ' rules:\n'
+ ' here: {\n'
+ ' foo: 1,\n'
+ ' bar: 2\n'
+ ' }\n'
+ ' there: {\n'
+ ' foo: 1,\n'
+ ' bar: 2\n'
+ ' }\n', conf, problem1=(7, 7), problem2=(11, 3))
+ conf = 'indentation: {spaces: 2}'
+ self.check('---\n'
+ 'a: {\n'
+ ' x: 1,\n'
+ ' y, z: 1\n'
+ '}\n', conf, problem=(3, 4))
+ self.check('---\n'
+ 'a: [\n'
+ ' x,\n'
+ ' y, z\n'
+ ']\n', conf, problem=(3, 4))
+
+ def test_cleared_flows(self):
+ # flow:
+ # [
+ # ...
+ # ]
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ 'top:\n'
+ ' rules:\n'
+ ' {\n'
+ ' foo: 1,\n'
+ ' bar: 2\n'
+ ' }\n', conf)
+ self.check('---\n'
+ 'top:\n'
+ ' rules:\n'
+ ' {\n'
+ ' foo: 1,\n'
+ ' bar: 2\n'
+ ' }\n', conf, problem=(5, 8))
+ self.check('---\n'
+ 'top:\n'
+ ' rules:\n'
+ ' {\n'
+ ' foo: 1,\n'
+ ' bar: 2\n'
+ ' }\n', conf, problem=(4, 4))
+ self.check('---\n'
+ 'top:\n'
+ ' rules:\n'
+ ' {\n'
+ ' foo: 1,\n'
+ ' bar: 2\n'
+ ' }\n', conf, problem=(7, 4))
+ self.check('---\n'
+ 'top:\n'
+ ' rules:\n'
+ ' {\n'
+ ' foo: 1,\n'
+ ' bar: 2\n'
+ ' }\n', conf, problem=(7, 6))
+ self.check('---\n'
+ 'top:\n'
+ ' [\n'
+ ' a, b, c\n'
+ ' ]\n', conf)
+ self.check('---\n'
+ 'top:\n'
+ ' [\n'
+ ' a, b, c\n'
+ ' ]\n', conf, problem=(4, 6))
+ self.check('---\n'
+ 'top:\n'
+ ' [\n'
+ ' a, b, c\n'
+ ' ]\n', conf, problem=(4, 6))
+ self.check('---\n'
+ 'top:\n'
+ ' [\n'
+ ' a, b, c\n'
+ ' ]\n', conf, problem=(5, 4))
+ self.check('---\n'
+ 'top:\n'
+ ' rules: [\n'
+ ' {\n'
+ ' foo: 1\n'
+ ' },\n'
+ ' {\n'
+ ' foo: 2,\n'
+ ' bar: [\n'
+ ' a, b, c\n'
+ ' ],\n'
+ ' },\n'
+ ' ]\n', conf)
+ self.check('---\n'
+ 'top:\n'
+ ' rules: [\n'
+ ' {\n'
+ ' foo: 1\n'
+ ' },\n'
+ ' {\n'
+ ' foo: 2,\n'
+ ' bar: [\n'
+ ' a, b, c\n'
+ ' ],\n'
+ ' },\n'
+ ']\n', conf, problem1=(5, 6), problem2=(6, 6),
+ problem3=(9, 9), problem4=(11, 7), problem5=(13, 1))
+
+ def test_under_indented(self):
+ conf = 'indentation: {spaces: 2, indent-sequences: consistent}'
+ self.check('---\n'
+ 'object:\n'
+ ' val: 1\n'
+ '...\n', conf, problem=(3, 2))
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ '...\n', conf, problem=(4, 4))
+ self.check('---\n'
+ 'object:\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ '...\n', conf, problem=(5, 6, 'syntax'))
+ conf = 'indentation: {spaces: 4, indent-sequences: consistent}'
+ self.check('---\n'
+ 'object:\n'
+ ' val: 1\n'
+ '...\n', conf, problem=(3, 4))
+ self.check('---\n'
+ '- el1\n'
+ '- el2:\n'
+ ' - subel\n'
+ '...\n', conf, problem=(4, 4))
+ self.check('---\n'
+ 'object:\n'
+ ' k3:\n'
+ ' - 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}'
+ self.check('---\n'
+ 'object:\n'
+ ' val: 1\n'
+ '...\n', conf, problem=(3, 4))
+ self.check('---\n'
+ 'object:\n'
+ ' k1:\n'
+ ' - a\n'
+ '...\n', conf, problem=(4, 6))
+ self.check('---\n'
+ 'object:\n'
+ ' k3:\n'
+ ' - name: Unix\n'
+ ' date: 1969\n'
+ '...\n', conf, problem=(5, 12, 'syntax'))
+ conf = 'indentation: {spaces: 4, indent-sequences: consistent}'
+ self.check('---\n'
+ 'object:\n'
+ ' val: 1\n'
+ '...\n', conf, problem=(3, 6))
+ self.check('---\n'
+ ' object:\n'
+ ' val: 1\n'
+ '...\n', conf, problem=(2, 2))
+ self.check('---\n'
+ '- el1\n'
+ '- el2:\n'
+ ' - subel\n'
+ '...\n', conf, problem=(4, 6))
+ self.check('---\n'
+ '- el1\n'
+ '- el2:\n'
+ ' - subel\n'
+ '...\n', conf, problem=(4, 15))
+ self.check('---\n'
+ ' - el1\n'
+ ' - el2:\n'
+ ' - subel\n'
+ '...\n', conf,
+ problem=(2, 3))
+ self.check('---\n'
+ 'object:\n'
+ ' k3:\n'
+ ' - name: Linux\n'
+ ' date: 1991\n'
+ '...\n', conf, problem=(5, 16, 'syntax'))
+ conf = 'indentation: {spaces: 4, indent-sequences: whatever}'
+ self.check('---\n'
+ ' - el1\n'
+ ' - el2:\n'
+ ' - 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}'
+ self.check('---\n'
+ 'long_string: >\n'
+ ' bla bla blah\n'
+ ' blah bla bla\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- long_string: >\n'
+ ' bla bla blah\n'
+ ' blah bla bla\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'obj:\n'
+ ' - long_string: >\n'
+ ' bla bla blah\n'
+ ' blah bla bla\n'
+ '...\n', conf)
+
+ def test_empty_value(self):
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ 'key1:\n'
+ 'key2: not empty\n'
+ 'key3:\n'
+ '...\n', conf)
+ self.check('---\n'
+ '-\n'
+ '- item 2\n'
+ '-\n'
+ '...\n', conf)
+
+ def test_nested_collections(self):
+ conf = 'indentation: {spaces: 2}'
+ self.check('---\n'
+ '- o:\n'
+ ' k1: v1\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- o:\n'
+ ' k1: v1\n'
+ '...\n', conf, problem=(3, 2, 'syntax'))
+ self.check('---\n'
+ '- o:\n'
+ ' k1: v1\n'
+ '...\n', conf, problem=(3, 4))
+ conf = 'indentation: {spaces: 4}'
+ self.check('---\n'
+ '- o:\n'
+ ' k1: v1\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- o:\n'
+ ' k1: v1\n'
+ '...\n', conf, problem=(3, 6))
+ self.check('---\n'
+ '- o:\n'
+ ' k1: v1\n'
+ '...\n', conf, problem=(3, 8))
+ self.check('---\n'
+ '- - - - item\n'
+ ' - elem 1\n'
+ ' - elem 2\n'
+ ' - - - - - very nested: a\n'
+ ' key: value\n'
+ '...\n', conf)
+ self.check('---\n'
+ ' - - - - item\n'
+ ' - elem 1\n'
+ ' - elem 2\n'
+ ' - - - - - very nested: a\n'
+ ' key: value\n'
+ '...\n', conf, problem=(2, 2))
+
+ def test_nested_collections_with_spaces_consistent(self):
+ """Tests behavior of {spaces: consistent} in nested collections to
+ ensure wrong-indentation is properly caught--especially when the
+ expected indent value is initially unknown. For details, see
+ https://github.com/adrienverge/yamllint/issues/485.
+ """
+ conf = ('indentation: {spaces: consistent,\n'
+ ' indent-sequences: true}')
+ self.check('---\n'
+ '- item:\n'
+ ' - elem\n'
+ '- item:\n'
+ ' - elem\n'
+ '...\n', conf, problem=(3, 3))
+ conf = ('indentation: {spaces: consistent,\n'
+ ' indent-sequences: false}')
+ self.check('---\n'
+ '- item:\n'
+ ' - elem\n'
+ '- item:\n'
+ ' - elem\n'
+ '...\n', conf, problem=(5, 5))
+ conf = ('indentation: {spaces: consistent,\n'
+ ' indent-sequences: consistent}')
+ self.check('---\n'
+ '- item:\n'
+ ' - elem\n'
+ '- item:\n'
+ ' - elem\n'
+ '...\n', conf, problem=(5, 5))
+ conf = ('indentation: {spaces: consistent,\n'
+ ' indent-sequences: whatever}')
+ self.check('---\n'
+ '- item:\n'
+ ' - elem\n'
+ '- item:\n'
+ ' - elem\n'
+ '...\n', conf)
+
+ def test_return(self):
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ 'a:\n'
+ ' b:\n'
+ ' c:\n'
+ ' d:\n'
+ ' e:\n'
+ ' f:\n'
+ 'g:\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'a:\n'
+ ' b:\n'
+ ' c:\n'
+ ' d:\n'
+ '...\n', conf, problem=(5, 4, 'syntax'))
+ self.check('---\n'
+ 'a:\n'
+ ' b:\n'
+ ' c:\n'
+ ' d:\n'
+ '...\n', conf, problem=(5, 2, 'syntax'))
+
+ def test_first_line(self):
+ conf = ('indentation: {spaces: consistent}\n'
+ 'document-start: disable\n')
+ self.check(' a: 1\n', conf, problem=(1, 3))
+
+ def test_explicit_block_mappings(self):
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ 'object:\n'
+ ' ? key\n'
+ ' : value\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' ? key\n'
+ ' :\n'
+ ' value\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' ?\n'
+ ' key\n'
+ ' : value\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' ?\n'
+ ' key\n'
+ ' :\n'
+ ' value\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- ? key\n'
+ ' : value\n', conf)
+ self.check('---\n'
+ '- ? key\n'
+ ' :\n'
+ ' value\n'
+ '...\n', conf)
+ self.check('---\n'
+ '- ?\n'
+ ' key\n'
+ ' : value\n', conf)
+ self.check('---\n'
+ '- ?\n'
+ ' key\n'
+ ' :\n'
+ ' value\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'object:\n'
+ ' ? key\n'
+ ' :\n'
+ ' value\n'
+ '...\n', conf, problem=(5, 8))
+ self.check('---\n'
+ '- - ?\n'
+ ' key\n'
+ ' :\n'
+ ' value\n'
+ '...\n', conf, problem=(5, 7))
+ self.check('---\n'
+ 'object:\n'
+ ' ?\n'
+ ' key\n'
+ ' :\n'
+ ' value\n'
+ '...\n', conf, problem1=(4, 8), problem2=(6, 10))
+ self.check('---\n'
+ 'object:\n'
+ ' ?\n'
+ ' key\n'
+ ' :\n'
+ ' value\n'
+ '...\n', conf, problem1=(4, 10), problem2=(6, 8))
+
+ def test_clear_sequence_item(self):
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ '-\n'
+ ' string\n'
+ '-\n'
+ ' map: ping\n'
+ '-\n'
+ ' - sequence\n'
+ ' -\n'
+ ' nested\n'
+ ' -\n'
+ ' >\n'
+ ' multi\n'
+ ' line\n'
+ '...\n', conf)
+ self.check('---\n'
+ '-\n'
+ ' string\n'
+ '-\n'
+ ' string\n', conf, problem=(5, 4))
+ self.check('---\n'
+ '-\n'
+ ' map: ping\n'
+ '-\n'
+ ' map: ping\n', conf, problem=(5, 4))
+ self.check('---\n'
+ '-\n'
+ ' - sequence\n'
+ '-\n'
+ ' - sequence\n', conf, problem=(5, 4))
+ self.check('---\n'
+ '-\n'
+ ' -\n'
+ ' nested\n'
+ ' -\n'
+ ' nested\n', conf, problem1=(4, 4), problem2=(6, 6))
+ self.check('---\n'
+ '-\n'
+ ' -\n'
+ ' >\n'
+ ' multi\n'
+ ' line\n'
+ '...\n', conf, problem=(4, 6))
+ conf = 'indentation: {spaces: 2}'
+ self.check('---\n'
+ '-\n'
+ ' string\n'
+ '-\n'
+ ' string\n', conf, problem1=(3, 2), problem2=(5, 4))
+ self.check('---\n'
+ '-\n'
+ ' map: ping\n'
+ '-\n'
+ ' map: ping\n', conf, problem1=(3, 2), problem2=(5, 4))
+ self.check('---\n'
+ '-\n'
+ ' - sequence\n'
+ '-\n'
+ ' - sequence\n', conf, problem1=(3, 2), problem2=(5, 4))
+ self.check('---\n'
+ '-\n'
+ ' -\n'
+ ' nested\n'
+ ' -\n'
+ ' nested\n', conf, problem1=(4, 4), problem2=(6, 6))
+
+ def test_anchors(self):
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ 'key: &anchor value\n', conf)
+ self.check('---\n'
+ 'key: &anchor\n'
+ ' value\n', conf)
+ self.check('---\n'
+ '- &anchor value\n', conf)
+ self.check('---\n'
+ '- &anchor\n'
+ ' value\n', conf)
+ self.check('---\n'
+ 'key: &anchor [1,\n'
+ ' 2]\n', conf)
+ self.check('---\n'
+ 'key: &anchor\n'
+ ' [1,\n'
+ ' 2]\n', conf)
+ self.check('---\n'
+ 'key: &anchor\n'
+ ' - 1\n'
+ ' - 2\n', conf)
+ self.check('---\n'
+ '- &anchor [1,\n'
+ ' 2]\n', conf)
+ self.check('---\n'
+ '- &anchor\n'
+ ' [1,\n'
+ ' 2]\n', conf)
+ self.check('---\n'
+ '- &anchor\n'
+ ' - 1\n'
+ ' - 2\n', conf)
+ self.check('---\n'
+ 'key:\n'
+ ' &anchor1\n'
+ ' value\n', conf)
+ self.check('---\n'
+ 'pre:\n'
+ ' &anchor1 0\n'
+ '&anchor2 key:\n'
+ ' value\n', conf)
+ self.check('---\n'
+ 'machine0:\n'
+ ' /etc/hosts: &ref-etc-hosts\n'
+ ' content:\n'
+ ' - 127.0.0.1: localhost\n'
+ ' - ::1: localhost\n'
+ ' mode: 0644\n'
+ 'machine1:\n'
+ ' /etc/hosts: *ref-etc-hosts\n', conf)
+ self.check('---\n'
+ 'list:\n'
+ ' - k: v\n'
+ ' - &a truc\n'
+ ' - &b\n'
+ ' truc\n'
+ ' - k: *a\n', conf)
+
+ def test_tags(self):
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ '-\n'
+ ' "flow in block"\n'
+ '- >\n'
+ ' Block scalar\n'
+ '- !!map # Block collection\n'
+ ' foo: bar\n', conf)
+
+ conf = 'indentation: {spaces: consistent, indent-sequences: false}'
+ self.check('---\n'
+ 'sequence: !!seq\n'
+ '- entry\n'
+ '- !!seq\n'
+ ' - nested\n', conf)
+ self.check('---\n'
+ 'mapping: !!map\n'
+ ' foo: bar\n'
+ 'Block style: !!map\n'
+ ' Clark: Evans\n'
+ ' Ingy: döt Net\n'
+ ' Oren: Ben-Kiki\n', conf)
+ self.check('---\n'
+ 'Flow style: !!map {Clark: Evans, Ingy: döt Net}\n'
+ 'Block style: !!seq\n'
+ '- Clark Evans\n'
+ '- Ingy döt Net\n', conf)
+
+ def test_flows_imbrication(self):
+ conf = 'indentation: {spaces: consistent}'
+ self.check('---\n'
+ '[val]: value\n', conf)
+ self.check('---\n'
+ '{key}: value\n', conf)
+ self.check('---\n'
+ '{key: val}: value\n', conf)
+ self.check('---\n'
+ '[[val]]: value\n', conf)
+ self.check('---\n'
+ '{{key}}: value\n', conf)
+ self.check('---\n'
+ '{{key: val1}: val2}: value\n', conf)
+ self.check('---\n'
+ '- [val, {{key: val}: val}]: value\n'
+ '- {[val,\n'
+ ' {{key: val}: val}]}\n'
+ '- {[val,\n'
+ ' {{key: val,\n'
+ ' key2}}]}\n'
+ '- {{{{{moustaches}}}}}\n'
+ '- {{{{{moustache,\n'
+ ' moustache},\n'
+ ' moustache}},\n'
+ ' moustache}}\n', conf)
+ self.check('---\n'
+ '- {[val,\n'
+ ' {{key: val}: val}]}\n',
+ conf, problem=(3, 6))
+ self.check('---\n'
+ '- {[val,\n'
+ ' {{key: val,\n'
+ ' key2}}]}\n',
+ conf, problem=(4, 6))
+ self.check('---\n'
+ '- {{{{{moustache,\n'
+ ' moustache},\n'
+ ' moustache}},\n'
+ ' moustache}}\n',
+ conf, problem1=(4, 8), problem2=(5, 4))
+
+
+class ScalarIndentationTestCase(RuleTestCase):
+ rule_id = 'indentation'
+
+ def test_basics_plain(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: false}\n'
+ 'document-start: disable\n')
+ self.check('multi\n'
+ 'line\n', conf)
+ self.check('multi\n'
+ ' line\n', conf)
+ self.check('- multi\n'
+ ' line\n', conf)
+ self.check('- multi\n'
+ ' line\n', conf)
+ self.check('a key: multi\n'
+ ' line\n', conf)
+ self.check('a key: multi\n'
+ ' line\n', conf)
+ self.check('a key: multi\n'
+ ' line\n', conf)
+ self.check('a key:\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('- C code: void main() {\n'
+ ' printf("foo");\n'
+ ' }\n', conf)
+ self.check('- C code:\n'
+ ' void main() {\n'
+ ' printf("foo");\n'
+ ' }\n', conf)
+
+ def test_check_multi_line_plain(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('multi\n'
+ ' line\n', conf, problem=(2, 2))
+ self.check('- multi\n'
+ ' line\n', conf, problem=(2, 4))
+ self.check('a key: multi\n'
+ ' line\n', conf, problem=(2, 3))
+ self.check('a key: multi\n'
+ ' line\n', conf, problem=(2, 9))
+ self.check('a key:\n'
+ ' multi\n'
+ ' line\n', conf, problem=(3, 4))
+ self.check('- C code: void main() {\n'
+ ' printf("foo");\n'
+ ' }\n', conf, problem=(2, 15))
+ self.check('- C code:\n'
+ ' void main() {\n'
+ ' printf("foo");\n'
+ ' }\n', conf, problem=(3, 9))
+
+ def test_basics_quoted(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: false}\n'
+ 'document-start: disable\n')
+ self.check('"multi\n'
+ ' line"\n', conf)
+ self.check('- "multi\n'
+ ' line"\n', conf)
+ self.check('a key: "multi\n'
+ ' line"\n', conf)
+ self.check('a key:\n'
+ ' "multi\n'
+ ' line"\n', conf)
+ self.check('- jinja2: "{% if ansible is defined %}\n'
+ ' {{ ansible }}\n'
+ ' {% else %}\n'
+ ' {{ chef }}\n'
+ ' {% endif %}"\n', conf)
+ self.check('- jinja2:\n'
+ ' "{% if ansible is defined %}\n'
+ ' {{ ansible }}\n'
+ ' {% else %}\n'
+ ' {{ chef }}\n'
+ ' {% endif %}"\n', conf)
+ self.check('["this is a very long line\n'
+ ' that needs to be split",\n'
+ ' "other line"]\n', conf)
+ self.check('["multi\n'
+ ' line 1", "multi\n'
+ ' line 2"]\n', conf)
+
+ def test_check_multi_line_quoted(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('"multi\n'
+ 'line"\n', conf, problem=(2, 1))
+ self.check('"multi\n'
+ ' line"\n', conf, problem=(2, 3))
+ self.check('- "multi\n'
+ ' line"\n', conf, problem=(2, 3))
+ self.check('- "multi\n'
+ ' line"\n', conf, problem=(2, 5))
+ self.check('a key: "multi\n'
+ ' line"\n', conf, problem=(2, 3))
+ self.check('a key: "multi\n'
+ ' line"\n', conf, problem=(2, 8))
+ self.check('a key: "multi\n'
+ ' line"\n', conf, problem=(2, 10))
+ self.check('a key:\n'
+ ' "multi\n'
+ ' line"\n', conf, problem=(3, 3))
+ self.check('a key:\n'
+ ' "multi\n'
+ ' line"\n', conf, problem=(3, 5))
+ self.check('- jinja2: "{% if ansible is defined %}\n'
+ ' {{ ansible }}\n'
+ ' {% else %}\n'
+ ' {{ chef }}\n'
+ ' {% endif %}"\n', conf,
+ problem1=(2, 14), problem2=(4, 14))
+ self.check('- jinja2:\n'
+ ' "{% if ansible is defined %}\n'
+ ' {{ ansible }}\n'
+ ' {% else %}\n'
+ ' {{ chef }}\n'
+ ' {% endif %}"\n', conf,
+ problem1=(3, 8), problem2=(5, 8))
+ self.check('["this is a very long line\n'
+ ' that needs to be split",\n'
+ ' "other line"]\n', conf)
+ self.check('["this is a very long line\n'
+ ' that needs to be split",\n'
+ ' "other line"]\n', conf, problem=(2, 2))
+ self.check('["this is a very long line\n'
+ ' that needs to be split",\n'
+ ' "other line"]\n', conf, problem=(2, 4))
+ self.check('["multi\n'
+ ' line 1", "multi\n'
+ ' line 2"]\n', conf)
+ self.check('["multi\n'
+ ' line 1", "multi\n'
+ ' line 2"]\n', conf, problem=(3, 12))
+ self.check('["multi\n'
+ ' line 1", "multi\n'
+ ' line 2"]\n', conf, problem=(3, 14))
+
+ def test_basics_folded_style(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: false}\n'
+ 'document-start: disable\n')
+ self.check('>\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('- >\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('- key: >\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('- key:\n'
+ ' >\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('- ? >\n'
+ ' multi-line\n'
+ ' key\n'
+ ' : >\n'
+ ' multi-line\n'
+ ' value\n', conf)
+ self.check('- ?\n'
+ ' >\n'
+ ' multi-line\n'
+ ' key\n'
+ ' :\n'
+ ' >\n'
+ ' multi-line\n'
+ ' value\n', conf)
+ self.check('- jinja2: >\n'
+ ' {% if ansible is defined %}\n'
+ ' {{ ansible }}\n'
+ ' {% else %}\n'
+ ' {{ chef }}\n'
+ ' {% endif %}\n', conf)
+
+ def test_check_multi_line_folded_style(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('>\n'
+ ' multi\n'
+ ' line\n', conf, problem=(3, 4))
+ self.check('- >\n'
+ ' multi\n'
+ ' line\n', conf, problem=(3, 6))
+ self.check('- key: >\n'
+ ' multi\n'
+ ' line\n', conf, problem=(3, 6))
+ self.check('- key:\n'
+ ' >\n'
+ ' multi\n'
+ ' line\n', conf, problem=(4, 8))
+ self.check('- ? >\n'
+ ' multi-line\n'
+ ' key\n'
+ ' : >\n'
+ ' multi-line\n'
+ ' value\n', conf,
+ problem1=(3, 8), problem2=(6, 8))
+ self.check('- ?\n'
+ ' >\n'
+ ' multi-line\n'
+ ' key\n'
+ ' :\n'
+ ' >\n'
+ ' multi-line\n'
+ ' value\n', conf,
+ problem1=(4, 8), problem2=(8, 8))
+ self.check('- jinja2: >\n'
+ ' {% if ansible is defined %}\n'
+ ' {{ ansible }}\n'
+ ' {% else %}\n'
+ ' {{ chef }}\n'
+ ' {% endif %}\n', conf,
+ problem1=(3, 7), problem2=(5, 7))
+
+ def test_basics_literal_style(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: false}\n'
+ 'document-start: disable\n')
+ self.check('|\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('- |\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('- key: |\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('- key:\n'
+ ' |\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('- ? |\n'
+ ' multi-line\n'
+ ' key\n'
+ ' : |\n'
+ ' multi-line\n'
+ ' value\n', conf)
+ self.check('- ?\n'
+ ' |\n'
+ ' multi-line\n'
+ ' key\n'
+ ' :\n'
+ ' |\n'
+ ' multi-line\n'
+ ' value\n', conf)
+ self.check('- jinja2: |\n'
+ ' {% if ansible is defined %}\n'
+ ' {{ ansible }}\n'
+ ' {% else %}\n'
+ ' {{ chef }}\n'
+ ' {% endif %}\n', conf)
+
+ def test_check_multi_line_literal_style(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('|\n'
+ ' multi\n'
+ ' line\n', conf, problem=(3, 4))
+ self.check('- |\n'
+ ' multi\n'
+ ' line\n', conf, problem=(3, 6))
+ self.check('- key: |\n'
+ ' multi\n'
+ ' line\n', conf, problem=(3, 6))
+ self.check('- key:\n'
+ ' |\n'
+ ' multi\n'
+ ' line\n', conf, problem=(4, 8))
+ self.check('- ? |\n'
+ ' multi-line\n'
+ ' key\n'
+ ' : |\n'
+ ' multi-line\n'
+ ' value\n', conf,
+ problem1=(3, 8), problem2=(6, 8))
+ self.check('- ?\n'
+ ' |\n'
+ ' multi-line\n'
+ ' key\n'
+ ' :\n'
+ ' |\n'
+ ' multi-line\n'
+ ' value\n', conf,
+ problem1=(4, 8), problem2=(8, 8))
+ self.check('- jinja2: |\n'
+ ' {% if ansible is defined %}\n'
+ ' {{ ansible }}\n'
+ ' {% else %}\n'
+ ' {{ chef }}\n'
+ ' {% endif %}\n', conf,
+ problem1=(3, 7), problem2=(5, 7))
+
+ # The following "paragraph" examples are inspired from
+ # http://stackoverflow.com/questions/3790454/in-yaml-how-do-i-break-a-string-over-multiple-lines
+
+ def test_paragraph_plain(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('- long text: very "long"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\n', conf)
+ self.check('- long text: very "long"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\n', conf,
+ problem1=(2, 5), problem2=(4, 5), problem3=(5, 5))
+ self.check('- long text:\n'
+ ' very "long"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\n', conf)
+
+ def test_paragraph_double_quoted(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('- long text: "very \\"long\\"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces."\n', conf)
+ self.check('- long text: "very \\"long\\"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces."\n', conf,
+ problem1=(2, 5), problem2=(4, 5), problem3=(5, 5))
+ self.check('- long text: "very \\"long\\"\n'
+ '\'string\' with\n'
+ '\n'
+ 'paragraph gap, \\n and\n'
+ 'spaces."\n', conf,
+ problem1=(2, 1), problem2=(4, 1), problem3=(5, 1))
+ self.check('- long text:\n'
+ ' "very \\"long\\"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces."\n', conf)
+
+ def test_paragraph_single_quoted(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('- long text: \'very "long"\n'
+ ' \'\'string\'\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\'\n', conf)
+ self.check('- long text: \'very "long"\n'
+ ' \'\'string\'\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\'\n', conf,
+ problem1=(2, 5), problem2=(4, 5), problem3=(5, 5))
+ self.check('- long text: \'very "long"\n'
+ '\'\'string\'\' with\n'
+ '\n'
+ 'paragraph gap, \\n and\n'
+ 'spaces.\'\n', conf,
+ problem1=(2, 1), problem2=(4, 1), problem3=(5, 1))
+ self.check('- long text:\n'
+ ' \'very "long"\n'
+ ' \'\'string\'\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\'\n', conf)
+
+ def test_paragraph_folded(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('- long text: >\n'
+ ' very "long"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\n', conf)
+ self.check('- long text: >\n'
+ ' very "long"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\n', conf,
+ problem1=(3, 6), problem2=(5, 7), problem3=(6, 8))
+
+ def test_paragraph_literal(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('- long text: |\n'
+ ' very "long"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\n', conf)
+ self.check('- long text: |\n'
+ ' very "long"\n'
+ ' \'string\' with\n'
+ '\n'
+ ' paragraph gap, \\n and\n'
+ ' spaces.\n', conf,
+ problem1=(3, 6), problem2=(5, 7), problem3=(6, 8))
+
+ def test_consistent(self):
+ conf = ('indentation: {spaces: consistent,\n'
+ ' check-multi-line-strings: true}\n'
+ 'document-start: disable\n')
+ self.check('multi\n'
+ 'line\n', conf)
+ self.check('multi\n'
+ ' line\n', conf, problem=(2, 2))
+ self.check('- multi\n'
+ ' line\n', conf)
+ self.check('- multi\n'
+ ' line\n', conf, problem=(2, 4))
+ self.check('a key: multi\n'
+ ' line\n', conf, problem=(2, 3))
+ self.check('a key: multi\n'
+ ' line\n', conf, problem=(2, 9))
+ self.check('a key:\n'
+ ' multi\n'
+ ' line\n', conf, problem=(3, 4))
+ self.check('- C code: void main() {\n'
+ ' printf("foo");\n'
+ ' }\n', conf, problem=(2, 15))
+ self.check('- C code:\n'
+ ' void main() {\n'
+ ' printf("foo");\n'
+ ' }\n', conf, problem=(3, 9))
+ self.check('>\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('>\n'
+ ' multi\n'
+ ' line\n', conf)
+ self.check('>\n'
+ ' multi\n'
+ ' line\n', conf, problem=(3, 7))
diff --git a/tests/rules/test_key_duplicates.py b/tests/rules/test_key_duplicates.py
new file mode 100644
index 0000000..3f8a9e6
--- /dev/null
+++ b/tests/rules/test_key_duplicates.py
@@ -0,0 +1,181 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+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)
+ self.check('---\n'
+ 'Merge Keys are OK:\n'
+ 'anchor_one: &anchor_one\n'
+ ' one: one\n'
+ 'anchor_two: &anchor_two\n'
+ ' two: two\n'
+ 'anchor_reference:\n'
+ ' <<: *anchor_one\n'
+ ' <<: *anchor_two\n', conf)
+ self.check('---\n'
+ '{a: 1, b: 2}}\n', conf, problem=(2, 13, 'syntax'))
+ self.check('---\n'
+ '[a, b, c]]\n', conf, problem=(2, 10, 'syntax'))
+
+ def test_enabled(self):
+ conf = 'key-duplicates: enable'
+ 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))
+ self.check('---\n'
+ 'Merge Keys are OK:\n'
+ 'anchor_one: &anchor_one\n'
+ ' one: one\n'
+ 'anchor_two: &anchor_two\n'
+ ' two: two\n'
+ 'anchor_reference:\n'
+ ' <<: *anchor_one\n'
+ ' <<: *anchor_two\n', conf)
+ self.check('---\n'
+ '{a: 1, b: 2}}\n', conf, problem=(2, 13, 'syntax'))
+ self.check('---\n'
+ '[a, b, c]]\n', conf, problem=(2, 10, 'syntax'))
+
+ def test_key_tokens_in_flow_sequences(self):
+ conf = 'key-duplicates: enable'
+ self.check('---\n'
+ '[\n'
+ ' flow: sequence, with, key: value, mappings\n'
+ ']\n', conf)
diff --git a/tests/rules/test_key_ordering.py b/tests/rules/test_key_ordering.py
new file mode 100644
index 0000000..7d17603
--- /dev/null
+++ b/tests/rules/test_key_ordering.py
@@ -0,0 +1,149 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import locale
+
+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))
+
+ 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)
+
+ 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 = ('key-ordering: enable')
+ self.check('---\n'
+ 't-shirt: 1\n'
+ 'T-shirt: 2\n'
+ 't-shirts: 3\n'
+ 'T-shirts: 4\n', conf)
+ self.check('---\n'
+ 't-shirt: 1\n'
+ 't-shirts: 2\n'
+ 'T-shirt: 3\n'
+ 'T-shirts: 4\n', conf,
+ problem=(4, 1))
+
+ 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 = ('key-ordering: enable')
+ self.check('---\n'
+ 'hair: true\n'
+ 'haïr: true\n'
+ 'hais: true\n'
+ 'haïssable: true\n', conf)
+ self.check('---\n'
+ 'hais: true\n'
+ 'haïr: true\n', conf,
+ problem=(3, 1))
diff --git a/tests/rules/test_line_length.py b/tests/rules/test_line_length.py
new file mode 100644
index 0000000..ef68178
--- /dev/null
+++ b/tests/rules/test_line_length.py
@@ -0,0 +1,198 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class LineLengthTestCase(RuleTestCase):
+ rule_id = 'line-length'
+
+ def test_disabled(self):
+ conf = ('line-length: disable\n'
+ 'empty-lines: disable\n'
+ 'new-line-at-end-of-file: disable\n'
+ 'document-start: disable\n')
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check('---\n', conf)
+ self.check(81 * 'a', conf)
+ self.check('---\n' + 81 * 'a' + '\n', conf)
+ self.check(1000 * 'b', conf)
+ self.check('---\n' + 1000 * 'b' + '\n', conf)
+ self.check('content: |\n'
+ ' {% this line is' + 99 * ' really' + ' long %}\n',
+ conf)
+
+ def test_default(self):
+ conf = ('line-length: {max: 80}\n'
+ 'empty-lines: disable\n'
+ 'new-line-at-end-of-file: disable\n'
+ 'document-start: disable\n')
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check('---\n', conf)
+ self.check(80 * 'a', conf)
+ self.check('---\n' + 80 * 'a' + '\n', conf)
+ self.check(16 * 'aaaa ' + 'z', conf, problem=(1, 81))
+ self.check('---\n' + 16 * 'aaaa ' + 'z' + '\n', conf, problem=(2, 81))
+ self.check(1000 * 'word ' + 'end', conf, problem=(1, 81))
+ self.check('---\n' + 1000 * 'word ' + 'end\n', conf, problem=(2, 81))
+
+ def test_max_length_10(self):
+ conf = ('line-length: {max: 10}\n'
+ 'new-line-at-end-of-file: disable\n')
+ self.check('---\nABCD EFGHI', conf)
+ self.check('---\nABCD EFGHIJ', conf, problem=(2, 11))
+ self.check('---\nABCD EFGHIJ\n', conf, problem=(2, 11))
+
+ def test_spaces(self):
+ conf = ('line-length: {max: 80}\n'
+ 'new-line-at-end-of-file: disable\n'
+ 'trailing-spaces: disable\n')
+ self.check('---\n' + 81 * ' ', conf, problem=(2, 81))
+ self.check('---\n' + 81 * ' ' + '\n', conf, problem=(2, 81))
+
+ def test_non_breakable_word(self):
+ conf = 'line-length: {max: 20, allow-non-breakable-words: true}'
+ self.check('---\n' + 30 * 'A' + '\n', conf)
+ self.check('---\n'
+ 'this:\n'
+ ' is:\n'
+ ' - a:\n'
+ ' http://localhost/very/long/url\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'this:\n'
+ ' is:\n'
+ ' - a:\n'
+ ' # http://localhost/very/long/url\n'
+ ' comment\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'this:\n'
+ 'is:\n'
+ 'another:\n'
+ ' - https://localhost/very/very/long/url\n'
+ '...\n', conf)
+ self.check('---\n'
+ 'long_line: http://localhost/very/very/long/url\n', conf,
+ problem=(2, 21))
+
+ conf = 'line-length: {max: 20, allow-non-breakable-words: false}'
+ self.check('---\n' + 30 * 'A' + '\n', conf, problem=(2, 21))
+ self.check('---\n'
+ 'this:\n'
+ ' is:\n'
+ ' - a:\n'
+ ' http://localhost/very/long/url\n'
+ '...\n', conf, problem=(5, 21))
+ self.check('---\n'
+ 'this:\n'
+ ' is:\n'
+ ' - a:\n'
+ ' # http://localhost/very/long/url\n'
+ ' comment\n'
+ '...\n', conf, problem=(5, 21))
+ self.check('---\n'
+ 'this:\n'
+ 'is:\n'
+ 'another:\n'
+ ' - https://localhost/very/very/long/url\n'
+ '...\n', conf, problem=(5, 21))
+ self.check('---\n'
+ 'long_line: http://localhost/very/very/long/url\n'
+ '...\n', conf, problem=(2, 21))
+
+ conf = 'line-length: {max: 20, allow-non-breakable-words: true}'
+ self.check('---\n'
+ '# http://www.verylongurlurlurlurlurlurlurlurl.com\n'
+ 'key:\n'
+ ' subkey: value\n', conf)
+ self.check('---\n'
+ '## http://www.verylongurlurlurlurlurlurlurlurl.com\n'
+ 'key:\n'
+ ' subkey: value\n', conf)
+ self.check('---\n'
+ '# # http://www.verylongurlurlurlurlurlurlurlurl.com\n'
+ 'key:\n'
+ ' subkey: value\n', conf,
+ problem=(2, 21))
+ self.check('---\n'
+ '#A http://www.verylongurlurlurlurlurlurlurlurl.com\n'
+ 'key:\n'
+ ' subkey: value\n', conf,
+ problem1=(2, 2, 'comments'),
+ problem2=(2, 21, 'line-length'))
+
+ conf = ('line-length: {max: 20, allow-non-breakable-words: true}\n'
+ 'trailing-spaces: disable')
+ self.check('---\n'
+ 'loooooooooong+word+and+some+space+at+the+end \n',
+ conf, problem=(2, 21))
+
+ def test_non_breakable_inline_mappings(self):
+ conf = 'line-length: {max: 20, ' \
+ 'allow-non-breakable-inline-mappings: true}'
+ self.check('---\n'
+ 'long_line: http://localhost/very/very/long/url\n'
+ 'long line: http://localhost/very/very/long/url\n', conf)
+ self.check('---\n'
+ '- long line: http://localhost/very/very/long/url\n', conf)
+
+ self.check('---\n'
+ 'long_line: http://localhost/short/url + word\n'
+ 'long line: http://localhost/short/url + word\n',
+ conf, problem1=(2, 21), problem2=(3, 21))
+
+ conf = ('line-length: {max: 20,'
+ ' allow-non-breakable-inline-mappings: true}\n'
+ 'trailing-spaces: disable')
+ self.check('---\n'
+ 'long_line: and+some+space+at+the+end \n',
+ conf, problem=(2, 21))
+ self.check('---\n'
+ 'long line: and+some+space+at+the+end \n',
+ conf, problem=(2, 21))
+ self.check('---\n'
+ '- long line: and+some+space+at+the+end \n',
+ conf, problem=(2, 21))
+
+ # See https://github.com/adrienverge/yamllint/issues/21
+ conf = 'line-length: {allow-non-breakable-inline-mappings: true}'
+ self.check('---\n'
+ 'content: |\n'
+ ' {% this line is' + 99 * ' really' + ' long %}\n',
+ conf, problem=(3, 81))
+
+ def test_unicode(self):
+ conf = 'line-length: {max: 53}'
+ self.check('---\n'
+ '# This is a test to check if “line-length” works nice\n'
+ 'with: “unicode characters” that span across bytes! ↺\n',
+ conf)
+ conf = 'line-length: {max: 51}'
+ self.check('---\n'
+ '# This is a test to check if “line-length” works nice\n'
+ 'with: “unicode characters” that span across bytes! ↺\n',
+ conf, problem1=(2, 52), problem2=(3, 52))
+
+ 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_line_at_end_of_file.py b/tests/rules/test_new_line_at_end_of_file.py
new file mode 100644
index 0000000..10a0bf0
--- /dev/null
+++ b/tests/rules/test_new_line_at_end_of_file.py
@@ -0,0 +1,41 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class NewLineAtEndOfFileTestCase(RuleTestCase):
+ rule_id = 'new-line-at-end-of-file'
+
+ def test_disabled(self):
+ conf = ('new-line-at-end-of-file: disable\n'
+ 'empty-lines: disable\n'
+ 'document-start: disable\n')
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check('word', conf)
+ self.check('Sentence.\n', conf)
+
+ def test_enabled(self):
+ conf = ('new-line-at-end-of-file: enable\n'
+ 'empty-lines: disable\n'
+ 'document-start: disable\n')
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check('word', conf, problem=(1, 5))
+ self.check('Sentence.\n', conf)
+ self.check('---\n'
+ 'yaml: document\n'
+ '...', conf, problem=(3, 4))
diff --git a/tests/rules/test_new_lines.py b/tests/rules/test_new_lines.py
new file mode 100644
index 0000000..80334ea
--- /dev/null
+++ b/tests/rules/test_new_lines.py
@@ -0,0 +1,96 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from unittest import mock
+
+from tests.common import RuleTestCase
+
+
+class NewLinesTestCase(RuleTestCase):
+ rule_id = 'new-lines'
+
+ def test_disabled(self):
+ conf = ('new-line-at-end-of-file: disable\n'
+ 'new-lines: disable\n')
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check('\r', conf)
+ self.check('\r\n', conf)
+ self.check('---\ntext\n', conf)
+ self.check('---\r\ntext\r\n', conf)
+
+ def test_unix_type(self):
+ 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_unix_type_required_st_sp(self):
+ # If we find a CRLF when looking for Unix newlines, yamllint
+ # should always raise, regardless of logic with
+ # require-starting-space.
+ conf = ('new-line-at-end-of-file: disable\n'
+ 'new-lines: {type: unix}\n'
+ 'comments:\n'
+ ' require-starting-space: true\n')
+ self.check('---\r\n#\r\n', conf, problem=(1, 4))
+
+ def test_dos_type(self):
+ 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))
+ self.check('---\r\ntext\r\n', conf)
+
+ def test_platform_type(self):
+ conf = ('new-line-at-end-of-file: disable\n'
+ 'new-lines: {type: platform}\n')
+
+ self.check('', conf)
+
+ # mock the Linux new-line-character
+ with mock.patch('yamllint.rules.new_lines.linesep', '\n'):
+ 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))
+ self.check('---\r\ntext\n', conf, problem=(1, 4))
+ # FIXME: the following tests currently don't work
+ # because only the first line is checked for line-endings
+ # see: issue #475
+ # ---
+ # self.check('---\ntext\r\nfoo\n', conf, problem=(2, 4))
+ # self.check('---\ntext\r\n', conf, problem=(2, 4))
+
+ # mock the Windows new-line-character
+ with mock.patch('yamllint.rules.new_lines.linesep', '\r\n'):
+ self.check('\r\n', conf)
+ self.check('\n', conf, problem=(1, 1))
+ self.check('---\r\ntext\r\n', conf)
+ self.check('---\ntext\n', conf, problem=(1, 4))
+ self.check('---\ntext\r\n', conf, problem=(1, 4))
+ # FIXME: the following tests currently don't work
+ # because only the first line is checked for line-endings
+ # see: issue #475
+ # ---
+ # self.check('---\r\ntext\nfoo\r\n', conf, problem=(2, 4))
+ # self.check('---\r\ntext\n', conf, problem=(2, 4))
diff --git a/tests/rules/test_octal_values.py b/tests/rules/test_octal_values.py
new file mode 100644
index 0000000..be5b039
--- /dev/null
+++ b/tests/rules/test_octal_values.py
@@ -0,0 +1,80 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class OctalValuesTestCase(RuleTestCase):
+ rule_id = 'octal-values'
+
+ def test_disabled(self):
+ conf = ('octal-values: disable\n'
+ 'new-line-at-end-of-file: disable\n'
+ 'document-start: disable\n')
+ self.check('user-city: 010', conf)
+ self.check('user-city: 0o10', conf)
+
+ def test_implicit_octal_values(self):
+ conf = ('octal-values:\n'
+ ' forbid-implicit-octal: true\n'
+ ' forbid-explicit-octal: false\n'
+ 'new-line-at-end-of-file: disable\n'
+ 'document-start: disable\n')
+ self.check('after-tag: !custom_tag 010', conf)
+ self.check('user-city: 010', conf, problem=(1, 15))
+ self.check('user-city: abc', conf)
+ self.check('user-city: 010,0571', conf)
+ self.check("user-city: '010'", conf)
+ self.check('user-city: "010"', conf)
+ self.check('user-city:\n'
+ ' - 010', conf, problem=(2, 8))
+ self.check('user-city: [010]', conf, problem=(1, 16))
+ self.check('user-city: {beijing: 010}', conf, problem=(1, 25))
+ self.check('explicit-octal: 0o10', conf)
+ self.check('not-number: 0abc', conf)
+ self.check('zero: 0', conf)
+ self.check('hex-value: 0x10', conf)
+ self.check('number-values:\n'
+ ' - 0.10\n'
+ ' - .01\n'
+ ' - 0e3\n', conf)
+ self.check('with-decimal-digits: 012345678', conf)
+ self.check('with-decimal-digits: 012345679', conf)
+
+ def test_explicit_octal_values(self):
+ conf = ('octal-values:\n'
+ ' forbid-implicit-octal: false\n'
+ ' forbid-explicit-octal: true\n'
+ 'new-line-at-end-of-file: disable\n'
+ 'document-start: disable\n')
+ self.check('user-city: 0o10', conf, problem=(1, 16))
+ self.check('user-city: abc', conf)
+ self.check('user-city: 0o10,0571', conf)
+ self.check("user-city: '0o10'", conf)
+ self.check('user-city:\n'
+ ' - 0o10', conf, problem=(2, 9))
+ self.check('user-city: [0o10]', conf, problem=(1, 17))
+ self.check('user-city: {beijing: 0o10}', conf, problem=(1, 26))
+ self.check('implicit-octal: 010', conf)
+ self.check('not-number: 0oabc', conf)
+ self.check('zero: 0', conf)
+ self.check('hex-value: 0x10', conf)
+ self.check('number-values:\n'
+ ' - 0.10\n'
+ ' - .01\n'
+ ' - 0e3\n', conf)
+ self.check('user-city: "010"', conf)
+ self.check('with-decimal-digits: 0o012345678', conf)
+ self.check('with-decimal-digits: 0o012345679', conf)
diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py
new file mode 100644
index 0000000..543cc0d
--- /dev/null
+++ b/tests/rules/test_quoted_strings.py
@@ -0,0 +1,558 @@
+# Copyright (C) 2018 ClearScore
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+from yamllint import config
+
+
+class QuotedTestCase(RuleTestCase):
+ rule_id = 'quoted-strings'
+
+ def test_disabled(self):
+ conf = 'quoted-strings: disable'
+
+ self.check('---\n'
+ 'foo: bar\n', conf)
+ self.check('---\n'
+ 'foo: "bar"\n', conf)
+ self.check('---\n'
+ 'foo: \'bar\'\n', conf)
+ self.check('---\n'
+ 'bar: 123\n', conf)
+ self.check('---\n'
+ 'bar: "123"\n', conf)
+
+ def test_quote_type_any(self):
+ conf = 'quoted-strings: {quote-type: any}\n'
+
+ self.check('---\n'
+ 'boolean1: true\n'
+ 'number1: 123\n'
+ 'string1: foo\n' # fails
+ 'string2: "foo"\n'
+ 'string3: "true"\n'
+ 'string4: "123"\n'
+ 'string5: \'bar\'\n'
+ 'string6: !!str genericstring\n'
+ 'string7: !!str 456\n'
+ 'string8: !!str "quotedgenericstring"\n'
+ 'binary: !!binary binstring\n'
+ 'integer: !!int intstring\n'
+ 'boolean2: !!bool boolstring\n'
+ 'boolean3: !!bool "quotedboolstring"\n'
+ 'block-seq:\n'
+ ' - foo\n' # fails
+ ' - "foo"\n'
+ 'flow-seq: [foo, "foo"]\n' # fails
+ 'flow-map: {a: foo, b: "foo"}\n', # fails
+ conf, problem1=(4, 10), problem2=(17, 5),
+ problem3=(19, 12), problem4=(20, 15))
+ self.check('---\n'
+ 'multiline string 1: |\n'
+ ' line 1\n'
+ ' line 2\n'
+ 'multiline string 2: >\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 3:\n'
+ ' word 1\n' # fails
+ ' word 2\n'
+ 'multiline string 4:\n'
+ ' "word 1\\\n'
+ ' word 2"\n',
+ conf, problem1=(9, 3))
+
+ def test_quote_type_single(self):
+ conf = 'quoted-strings: {quote-type: single}\n'
+
+ self.check('---\n'
+ 'boolean1: true\n'
+ 'number1: 123\n'
+ 'string1: foo\n' # fails
+ 'string2: "foo"\n' # fails
+ 'string3: "true"\n' # fails
+ 'string4: "123"\n' # fails
+ 'string5: \'bar\'\n'
+ 'string6: !!str genericstring\n'
+ 'string7: !!str 456\n'
+ 'string8: !!str "quotedgenericstring"\n'
+ 'binary: !!binary binstring\n'
+ 'integer: !!int intstring\n'
+ 'boolean2: !!bool boolstring\n'
+ 'boolean3: !!bool "quotedboolstring"\n'
+ 'block-seq:\n'
+ ' - foo\n' # fails
+ ' - "foo"\n' # fails
+ 'flow-seq: [foo, "foo"]\n' # fails
+ 'flow-map: {a: foo, b: "foo"}\n', # fails
+ conf, problem1=(4, 10), problem2=(5, 10), problem3=(6, 10),
+ problem4=(7, 10), problem5=(17, 5), problem6=(18, 5),
+ problem7=(19, 12), problem8=(19, 17), problem9=(20, 15),
+ problem10=(20, 23))
+ self.check('---\n'
+ 'multiline string 1: |\n'
+ ' line 1\n'
+ ' line 2\n'
+ 'multiline string 2: >\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 3:\n'
+ ' word 1\n' # fails
+ ' word 2\n'
+ 'multiline string 4:\n'
+ ' "word 1\\\n'
+ ' word 2"\n',
+ conf, problem1=(9, 3), problem2=(12, 3))
+
+ def test_quote_type_double(self):
+ conf = 'quoted-strings: {quote-type: double}\n'
+
+ self.check('---\n'
+ 'boolean1: true\n'
+ 'number1: 123\n'
+ 'string1: foo\n' # fails
+ 'string2: "foo"\n'
+ 'string3: "true"\n'
+ 'string4: "123"\n'
+ 'string5: \'bar\'\n' # fails
+ 'string6: !!str genericstring\n'
+ 'string7: !!str 456\n'
+ 'string8: !!str "quotedgenericstring"\n'
+ 'binary: !!binary binstring\n'
+ 'integer: !!int intstring\n'
+ 'boolean2: !!bool boolstring\n'
+ 'boolean3: !!bool "quotedboolstring"\n'
+ 'block-seq:\n'
+ ' - foo\n' # fails
+ ' - "foo"\n'
+ 'flow-seq: [foo, "foo"]\n' # fails
+ 'flow-map: {a: foo, b: "foo"}\n', # fails
+ conf, problem1=(4, 10), problem2=(8, 10), problem3=(17, 5),
+ problem4=(19, 12), problem5=(20, 15))
+ self.check('---\n'
+ 'multiline string 1: |\n'
+ ' line 1\n'
+ ' line 2\n'
+ 'multiline string 2: >\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 3:\n'
+ ' word 1\n' # fails
+ ' word 2\n'
+ 'multiline string 4:\n'
+ ' "word 1\\\n'
+ ' word 2"\n',
+ conf, problem1=(9, 3))
+
+ def test_any_quotes_not_required(self):
+ conf = 'quoted-strings: {quote-type: any, required: false}\n'
+
+ self.check('---\n'
+ 'boolean1: true\n'
+ 'number1: 123\n'
+ 'string1: foo\n'
+ 'string2: "foo"\n'
+ 'string3: "true"\n'
+ 'string4: "123"\n'
+ 'string5: \'bar\'\n'
+ 'string6: !!str genericstring\n'
+ 'string7: !!str 456\n'
+ 'string8: !!str "quotedgenericstring"\n'
+ 'binary: !!binary binstring\n'
+ 'integer: !!int intstring\n'
+ 'boolean2: !!bool boolstring\n'
+ 'boolean3: !!bool "quotedboolstring"\n'
+ 'block-seq:\n'
+ ' - foo\n' # fails
+ ' - "foo"\n'
+ 'flow-seq: [foo, "foo"]\n' # fails
+ 'flow-map: {a: foo, b: "foo"}\n', # fails
+ conf)
+ self.check('---\n'
+ 'multiline string 1: |\n'
+ ' line 1\n'
+ ' line 2\n'
+ 'multiline string 2: >\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 3:\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 4:\n'
+ ' "word 1\\\n'
+ ' word 2"\n',
+ conf)
+
+ def test_single_quotes_not_required(self):
+ conf = 'quoted-strings: {quote-type: single, required: false}\n'
+
+ self.check('---\n'
+ 'boolean1: true\n'
+ 'number1: 123\n'
+ 'string1: foo\n'
+ 'string2: "foo"\n' # fails
+ 'string3: "true"\n' # fails
+ 'string4: "123"\n' # fails
+ 'string5: \'bar\'\n'
+ 'string6: !!str genericstring\n'
+ 'string7: !!str 456\n'
+ 'string8: !!str "quotedgenericstring"\n'
+ 'binary: !!binary binstring\n'
+ 'integer: !!int intstring\n'
+ 'boolean2: !!bool boolstring\n'
+ 'boolean3: !!bool "quotedboolstring"\n'
+ 'block-seq:\n'
+ ' - foo\n' # fails
+ ' - "foo"\n'
+ 'flow-seq: [foo, "foo"]\n' # fails
+ 'flow-map: {a: foo, b: "foo"}\n', # fails
+ conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10),
+ problem4=(18, 5), problem5=(19, 17), problem6=(20, 23))
+ self.check('---\n'
+ 'multiline string 1: |\n'
+ ' line 1\n'
+ ' line 2\n'
+ 'multiline string 2: >\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 3:\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 4:\n'
+ ' "word 1\\\n' # fails
+ ' word 2"\n',
+ conf, problem1=(12, 3))
+
+ def test_only_when_needed(self):
+ conf = 'quoted-strings: {required: only-when-needed}\n'
+
+ self.check('---\n'
+ 'boolean1: true\n'
+ 'number1: 123\n'
+ 'string1: foo\n'
+ 'string2: "foo"\n' # fails
+ 'string3: "true"\n'
+ 'string4: "123"\n'
+ 'string5: \'bar\'\n' # fails
+ 'string6: !!str genericstring\n'
+ 'string7: !!str 456\n'
+ 'string8: !!str "quotedgenericstring"\n'
+ 'binary: !!binary binstring\n'
+ 'integer: !!int intstring\n'
+ 'boolean2: !!bool boolstring\n'
+ 'boolean3: !!bool "quotedboolstring"\n'
+ 'block-seq:\n'
+ ' - foo\n'
+ ' - "foo"\n' # fails
+ 'flow-seq: [foo, "foo"]\n' # fails
+ 'flow-map: {a: foo, b: "foo"}\n', # fails
+ conf, problem1=(5, 10), problem2=(8, 10), problem3=(18, 5),
+ problem4=(19, 17), problem5=(20, 23))
+ self.check('---\n'
+ 'multiline string 1: |\n'
+ ' line 1\n'
+ ' line 2\n'
+ 'multiline string 2: >\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 3:\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 4:\n'
+ ' "word 1\\\n' # fails
+ ' word 2"\n',
+ conf, problem1=(12, 3))
+
+ def test_only_when_needed_single_quotes(self):
+ conf = ('quoted-strings: {quote-type: single,\n'
+ ' required: only-when-needed}\n')
+
+ self.check('---\n'
+ 'boolean1: true\n'
+ 'number1: 123\n'
+ 'string1: foo\n'
+ 'string2: "foo"\n' # fails
+ 'string3: "true"\n' # fails
+ 'string4: "123"\n' # fails
+ 'string5: \'bar\'\n' # fails
+ 'string6: !!str genericstring\n'
+ 'string7: !!str 456\n'
+ 'string8: !!str "quotedgenericstring"\n'
+ 'binary: !!binary binstring\n'
+ 'integer: !!int intstring\n'
+ 'boolean2: !!bool boolstring\n'
+ 'boolean3: !!bool "quotedboolstring"\n'
+ 'block-seq:\n'
+ ' - foo\n'
+ ' - "foo"\n' # fails
+ 'flow-seq: [foo, "foo"]\n' # fails
+ 'flow-map: {a: foo, b: "foo"}\n', # fails
+ conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10),
+ problem4=(8, 10), problem5=(18, 5), problem6=(19, 17),
+ problem7=(20, 23))
+ self.check('---\n'
+ 'multiline string 1: |\n'
+ ' line 1\n'
+ ' line 2\n'
+ 'multiline string 2: >\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 3:\n'
+ ' word 1\n'
+ ' word 2\n'
+ 'multiline string 4:\n'
+ ' "word 1\\\n' # fails
+ ' word 2"\n',
+ conf, problem1=(12, 3))
+
+ def test_only_when_needed_corner_cases(self):
+ conf = 'quoted-strings: {required: only-when-needed}\n'
+
+ self.check('---\n'
+ '- ""\n'
+ '- "- item"\n'
+ '- "key: value"\n'
+ '- "%H:%M:%S"\n'
+ '- "%wheel ALL=(ALL) NOPASSWD: ALL"\n'
+ '- \'"quoted"\'\n'
+ '- "\'foo\' == \'bar\'"\n'
+ '- "\'Mac\' in ansible_facts.product_name"\n'
+ '- \'foo # bar\'\n',
+ conf)
+ self.check('---\n'
+ 'k1: ""\n'
+ 'k2: "- item"\n'
+ 'k3: "key: value"\n'
+ 'k4: "%H:%M:%S"\n'
+ 'k5: "%wheel ALL=(ALL) NOPASSWD: ALL"\n'
+ 'k6: \'"quoted"\'\n'
+ 'k7: "\'foo\' == \'bar\'"\n'
+ 'k8: "\'Mac\' in ansible_facts.product_name"\n',
+ conf)
+
+ self.check('---\n'
+ '- ---\n'
+ '- "---"\n' # fails
+ '- ----------\n'
+ '- "----------"\n' # fails
+ '- :wq\n'
+ '- ":wq"\n', # fails
+ conf, problem1=(3, 3), problem2=(5, 3), problem3=(7, 3))
+ self.check('---\n'
+ 'k1: ---\n'
+ 'k2: "---"\n' # fails
+ 'k3: ----------\n'
+ 'k4: "----------"\n' # fails
+ 'k5: :wq\n'
+ 'k6: ":wq"\n', # fails
+ conf, problem1=(3, 5), problem2=(5, 5), problem3=(7, 5))
+
+ def test_only_when_needed_extras(self):
+ conf = ('quoted-strings:\n'
+ ' required: true\n'
+ ' extra-allowed: [^http://]\n')
+ self.assertRaises(config.YamlLintConfigError, self.check, '', conf)
+
+ conf = ('quoted-strings:\n'
+ ' required: true\n'
+ ' extra-required: [^http://]\n')
+ self.assertRaises(config.YamlLintConfigError, self.check, '', conf)
+
+ conf = ('quoted-strings:\n'
+ ' required: false\n'
+ ' extra-allowed: [^http://]\n')
+ self.assertRaises(config.YamlLintConfigError, self.check, '', conf)
+
+ conf = ('quoted-strings:\n'
+ ' required: true\n')
+ self.check('---\n'
+ '- 123\n'
+ '- "123"\n'
+ '- localhost\n' # fails
+ '- "localhost"\n'
+ '- http://localhost\n' # fails
+ '- "http://localhost"\n'
+ '- ftp://localhost\n' # fails
+ '- "ftp://localhost"\n',
+ conf, problem1=(4, 3), problem2=(6, 3), problem3=(8, 3))
+
+ conf = ('quoted-strings:\n'
+ ' required: only-when-needed\n'
+ ' extra-allowed: [^ftp://]\n'
+ ' extra-required: [^http://]\n')
+ self.check('---\n'
+ '- 123\n'
+ '- "123"\n'
+ '- localhost\n'
+ '- "localhost"\n' # fails
+ '- http://localhost\n' # fails
+ '- "http://localhost"\n'
+ '- ftp://localhost\n'
+ '- "ftp://localhost"\n',
+ conf, problem1=(5, 3), problem2=(6, 3))
+
+ conf = ('quoted-strings:\n'
+ ' required: false\n'
+ ' extra-required: [^http://, ^ftp://]\n')
+ self.check('---\n'
+ '- 123\n'
+ '- "123"\n'
+ '- localhost\n'
+ '- "localhost"\n'
+ '- http://localhost\n' # fails
+ '- "http://localhost"\n'
+ '- ftp://localhost\n' # fails
+ '- "ftp://localhost"\n',
+ conf, problem1=(6, 3), problem2=(8, 3))
+
+ conf = ('quoted-strings:\n'
+ ' required: only-when-needed\n'
+ ' extra-allowed: [^ftp://, ";$", " "]\n')
+ self.check('---\n'
+ '- localhost\n'
+ '- "localhost"\n' # fails
+ '- ftp://localhost\n'
+ '- "ftp://localhost"\n'
+ '- i=i+1\n'
+ '- "i=i+1"\n' # fails
+ '- i=i+2;\n'
+ '- "i=i+2;"\n'
+ '- foo\n'
+ '- "foo"\n' # fails
+ '- foo bar\n'
+ '- "foo bar"\n',
+ conf, problem1=(3, 3), problem2=(7, 3), problem3=(11, 3))
+
+ def test_octal_values(self):
+ conf = 'quoted-strings: {required: true}\n'
+
+ self.check('---\n'
+ '- 100\n'
+ '- 0100\n'
+ '- 0o100\n'
+ '- 777\n'
+ '- 0777\n'
+ '- 0o777\n'
+ '- 800\n'
+ '- 0800\n'
+ '- 0o800\n'
+ '- "0800"\n'
+ '- "0o800"\n',
+ conf,
+ problem1=(9, 3), problem2=(10, 3))
+
+ def test_allow_quoted_quotes(self):
+ conf = ('quoted-strings: {quote-type: single,\n'
+ ' required: false,\n'
+ ' allow-quoted-quotes: false}\n')
+ self.check('---\n'
+ 'foo1: "[barbaz]"\n' # fails
+ 'foo2: "[bar\'baz]"\n', # fails
+ conf, problem1=(2, 7), problem2=(3, 7))
+
+ conf = ('quoted-strings: {quote-type: single,\n'
+ ' required: false,\n'
+ ' allow-quoted-quotes: true}\n')
+ self.check('---\n'
+ 'foo1: "[barbaz]"\n' # fails
+ 'foo2: "[bar\'baz]"\n',
+ conf, problem1=(2, 7))
+
+ conf = ('quoted-strings: {quote-type: single,\n'
+ ' required: true,\n'
+ ' allow-quoted-quotes: false}\n')
+ self.check('---\n'
+ 'foo1: "[barbaz]"\n' # fails
+ 'foo2: "[bar\'baz]"\n', # fails
+ conf, problem1=(2, 7), problem2=(3, 7))
+
+ conf = ('quoted-strings: {quote-type: single,\n'
+ ' required: true,\n'
+ ' allow-quoted-quotes: true}\n')
+ self.check('---\n'
+ 'foo1: "[barbaz]"\n' # fails
+ 'foo2: "[bar\'baz]"\n',
+ conf, problem1=(2, 7))
+
+ conf = ('quoted-strings: {quote-type: single,\n'
+ ' required: only-when-needed,\n'
+ ' allow-quoted-quotes: false}\n')
+ self.check('---\n'
+ 'foo1: "[barbaz]"\n' # fails
+ 'foo2: "[bar\'baz]"\n', # fails
+ conf, problem1=(2, 7), problem2=(3, 7))
+
+ conf = ('quoted-strings: {quote-type: single,\n'
+ ' required: only-when-needed,\n'
+ ' allow-quoted-quotes: true}\n')
+ self.check('---\n'
+ 'foo1: "[barbaz]"\n' # fails
+ 'foo2: "[bar\'baz]"\n',
+ conf, problem1=(2, 7))
+
+ conf = ('quoted-strings: {quote-type: double,\n'
+ ' required: false,\n'
+ ' allow-quoted-quotes: false}\n')
+ self.check("---\n"
+ "foo1: '[barbaz]'\n" # fails
+ "foo2: '[bar\"baz]'\n", # fails
+ conf, problem1=(2, 7), problem2=(3, 7))
+
+ conf = ('quoted-strings: {quote-type: double,\n'
+ ' required: false,\n'
+ ' allow-quoted-quotes: true}\n')
+ self.check("---\n"
+ "foo1: '[barbaz]'\n" # fails
+ "foo2: '[bar\"baz]'\n",
+ conf, problem1=(2, 7))
+
+ conf = ('quoted-strings: {quote-type: double,\n'
+ ' required: true,\n'
+ ' allow-quoted-quotes: false}\n')
+ self.check("---\n"
+ "foo1: '[barbaz]'\n" # fails
+ "foo2: '[bar\"baz]'\n", # fails
+ conf, problem1=(2, 7), problem2=(3, 7))
+
+ conf = ('quoted-strings: {quote-type: double,\n'
+ ' required: true,\n'
+ ' allow-quoted-quotes: true}\n')
+ self.check("---\n"
+ "foo1: '[barbaz]'\n" # fails
+ "foo2: '[bar\"baz]'\n",
+ conf, problem1=(2, 7))
+
+ conf = ('quoted-strings: {quote-type: double,\n'
+ ' required: only-when-needed,\n'
+ ' allow-quoted-quotes: false}\n')
+ self.check("---\n"
+ "foo1: '[barbaz]'\n" # fails
+ "foo2: '[bar\"baz]'\n", # fails
+ conf, problem1=(2, 7), problem2=(3, 7))
+
+ conf = ('quoted-strings: {quote-type: double,\n'
+ ' required: only-when-needed,\n'
+ ' allow-quoted-quotes: true}\n')
+ self.check("---\n"
+ "foo1: '[barbaz]'\n" # fails
+ "foo2: '[bar\"baz]'\n",
+ conf, problem1=(2, 7))
+
+ conf = ('quoted-strings: {quote-type: any}\n')
+ self.check("---\n"
+ "foo1: '[barbaz]'\n"
+ "foo2: '[bar\"baz]'\n",
+ conf)
diff --git a/tests/rules/test_trailing_spaces.py b/tests/rules/test_trailing_spaces.py
new file mode 100644
index 0000000..016f56e
--- /dev/null
+++ b/tests/rules/test_trailing_spaces.py
@@ -0,0 +1,47 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class TrailingSpacesTestCase(RuleTestCase):
+ rule_id = 'trailing-spaces'
+
+ def test_disabled(self):
+ conf = 'trailing-spaces: disable'
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check(' \n', conf)
+ self.check('---\n'
+ 'some: text \n', conf)
+
+ def test_enabled(self):
+ conf = 'trailing-spaces: enable'
+ self.check('', conf)
+ self.check('\n', conf)
+ self.check(' \n', conf, problem=(1, 1))
+ self.check('\t\t\t\n', conf, problem=(1, 1, 'syntax'))
+ self.check('---\n'
+ 'some: text \n', conf, problem=(2, 11))
+ self.check('---\n'
+ 'some: text\t\n', conf, problem=(2, 11, 'syntax'))
+
+ def test_with_dos_new_lines(self):
+ conf = ('trailing-spaces: enable\n'
+ 'new-lines: {type: dos}\n')
+ self.check('---\r\n'
+ 'some: text\r\n', conf)
+ self.check('---\r\n'
+ 'some: text \r\n', conf, problem=(2, 11))
diff --git a/tests/rules/test_truthy.py b/tests/rules/test_truthy.py
new file mode 100644
index 0000000..c8c8b7a
--- /dev/null
+++ b/tests/rules/test_truthy.py
@@ -0,0 +1,145 @@
+# Copyright (C) 2016 Peter Ericson
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class TruthyTestCase(RuleTestCase):
+ rule_id = 'truthy'
+
+ def test_disabled(self):
+ conf = 'truthy: disable'
+ self.check('---\n'
+ '1: True\n', conf)
+ self.check('---\n'
+ 'True: 1\n', conf)
+
+ def test_enabled(self):
+ conf = 'truthy: enable\n'
+ self.check('---\n'
+ '1: True\n'
+ 'True: 1\n',
+ conf, problem1=(2, 4), problem2=(3, 1))
+ self.check('---\n'
+ '1: "True"\n'
+ '"True": 1\n', conf)
+ self.check('---\n'
+ '[\n'
+ ' true, false,\n'
+ ' "false", "FALSE",\n'
+ ' "true", "True",\n'
+ ' True, FALSE,\n'
+ ' on, OFF,\n'
+ ' NO, Yes\n'
+ ']\n', conf,
+ problem1=(6, 3), problem2=(6, 9),
+ 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'
+ 'string1: !!str True\n'
+ 'string2: !!str yes\n'
+ 'string3: !!str off\n'
+ 'encoded: !!binary |\n'
+ ' True\n'
+ ' OFF\n'
+ ' pad==\n' # this decodes as 'N\xbb\x9e8Qii'
+ 'boolean1: !!bool true\n'
+ 'boolean2: !!bool "false"\n'
+ 'boolean3: !!bool FALSE\n'
+ 'boolean4: !!bool True\n'
+ 'boolean5: !!bool off\n'
+ 'boolean6: !!bool NO\n',
+ conf)
+
+ def test_check_keys_disabled(self):
+ conf = ('truthy:\n'
+ ' allowed-values: []\n'
+ ' check-keys: false\n'
+ 'key-duplicates: disable\n')
+ self.check('---\n'
+ 'YES: 0\n'
+ 'Yes: 0\n'
+ 'yes: 0\n'
+ 'No: 0\n'
+ 'No: 0\n'
+ 'no: 0\n'
+ 'TRUE: 0\n'
+ 'True: 0\n'
+ 'true: 0\n'
+ 'FALSE: 0\n'
+ 'False: 0\n'
+ 'false: 0\n'
+ 'ON: 0\n'
+ 'On: 0\n'
+ 'on: 0\n'
+ 'OFF: 0\n'
+ 'Off: 0\n'
+ 'off: 0\n'
+ 'YES:\n'
+ ' Yes:\n'
+ ' yes:\n'
+ ' on: 0\n',
+ conf)
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..444f2f9
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,795 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from io import StringIO
+import fcntl
+import locale
+import os
+import pty
+import shutil
+import sys
+import tempfile
+import unittest
+
+from tests.common import build_temp_workspace, temp_workspace
+
+from yamllint import cli
+from yamllint import config
+
+
+class RunContext:
+ """Context manager for ``cli.run()`` to capture exit code and streams."""
+
+ def __init__(self, case):
+ self.stdout = self.stderr = None
+ self._raises_ctx = case.assertRaises(SystemExit)
+
+ def __enter__(self):
+ self._raises_ctx.__enter__()
+ sys.stdout = self.outstream = StringIO()
+ sys.stderr = self.errstream = StringIO()
+ return self
+
+ def __exit__(self, *exc_info):
+ self.stdout, sys.stdout = self.outstream.getvalue(), sys.__stdout__
+ self.stderr, sys.stderr = self.errstream.getvalue(), sys.__stderr__
+ return self._raises_ctx.__exit__(*exc_info)
+
+ @property
+ def returncode(self):
+ return self._raises_ctx.exception.code
+
+
+# Check system's UTF-8 availability
+def utf8_available():
+ try:
+ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
+ locale.setlocale(locale.LC_ALL, (None, None))
+ return True
+ except locale.Error: # pragma: no cover
+ return False
+
+
+class CommandLineTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.wd = build_temp_workspace({
+ # .yaml file at root
+ 'a.yaml': '---\n'
+ '- 1 \n'
+ '- 2',
+ # file with only one warning
+ 'warn.yaml': 'key: value\n',
+ # .yml file at root
+ 'empty.yml': '',
+ # file in dir
+ 'sub/ok.yaml': '---\n'
+ 'key: value\n',
+ # directory that looks like a yaml file
+ 'sub/directory.yaml/not-yaml.txt': '',
+ 'sub/directory.yaml/empty.yml': '',
+ # file in very nested dir
+ 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml': '---\n'
+ 'key: value\n'
+ 'key: other value\n',
+ # empty dir
+ 'empty-dir': [],
+ # non-YAML file
+ 'no-yaml.json': '---\n'
+ 'key: value\n',
+ # non-ASCII chars
+ 'non-ascii/éçäγλνπ¥/utf-8': (
+ '---\n'
+ '- hétérogénéité\n'
+ '# 19.99 €\n'
+ '- お早う御座います。\n'
+ '# الأَبْجَدِيَّة العَرَبِيَّة\n').encode(),
+ # dos line endings yaml
+ 'dos.yml': '---\r\n'
+ 'dos: true',
+ # different key-ordering by locale
+ 'c.yaml': '---\n'
+ 'A: true\n'
+ 'a: true',
+ 'en.yaml': '---\n'
+ 'a: true\n'
+ 'A: true'
+ })
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ shutil.rmtree(cls.wd)
+
+ @unittest.skipIf(not utf8_available() and sys.version_info < (3, 7),
+ 'UTF-8 paths not supported')
+ def test_find_files_recursively(self):
+ conf = config.YamlLintConfig('extends: default')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')],
+ )
+
+ items = [os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'empty-dir')]
+ self.assertEqual(
+ sorted(cli.find_files_recursively(items, conf)),
+ [os.path.join(self.wd, 'sub/ok.yaml')],
+ )
+
+ items = [os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 's')]
+ self.assertEqual(
+ sorted(cli.find_files_recursively(items, conf)),
+ [os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml')],
+ )
+
+ items = [os.path.join(self.wd, 'sub'),
+ os.path.join(self.wd, '/etc/another/file')]
+ self.assertEqual(
+ sorted(cli.find_files_recursively(items, conf)),
+ [os.path.join(self.wd, '/etc/another/file'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/ok.yaml')],
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*.yaml\' \n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*.yml\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*.json\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'no-yaml.json')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 'no-yaml.json'),
+ os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*.yaml\'\n'
+ ' - \'*\'\n'
+ ' - \'**\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 'no-yaml.json'),
+ os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'s/**\'\n'
+ ' - \'**/utf-8\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8')]
+ )
+
+ def test_run_with_bad_arguments(self):
+ with RunContext(self) as ctx:
+ cli.run(())
+ self.assertNotEqual(ctx.returncode, 0)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^usage')
+
+ with RunContext(self) as ctx:
+ cli.run(('--unknown-arg', ))
+ self.assertNotEqual(ctx.returncode, 0)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^usage')
+
+ with RunContext(self) as ctx:
+ cli.run(('-c', './conf.yaml', '-d', 'relaxed', 'file'))
+ self.assertNotEqual(ctx.returncode, 0)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(
+ ctx.stderr.splitlines()[-1],
+ r'^yamllint: error: argument -d\/--config-data: '
+ r'not allowed with argument -c\/--config-file$'
+ )
+
+ # checks if reading from stdin and files are mutually exclusive
+ with RunContext(self) as ctx:
+ cli.run(('-', 'file'))
+ self.assertNotEqual(ctx.returncode, 0)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^usage')
+
+ def test_run_with_bad_config(self):
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules: {a: b}', 'file'))
+ self.assertEqual(ctx.returncode, -1)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^invalid config: no such rule')
+
+ def test_run_with_empty_config(self):
+ with RunContext(self) as ctx:
+ cli.run(('-d', '', 'file'))
+ self.assertEqual(ctx.returncode, -1)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^invalid config: not a dict')
+
+ def test_run_with_implicit_extends_config(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'default', '-f', 'parsable', path))
+ expected_out = (f'{path}:1:1: [warning] missing document start "---" '
+ f'(document-start)\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (0, expected_out, ''))
+
+ def test_run_with_config_file(self):
+ with open(os.path.join(self.wd, 'config'), 'w') as f:
+ f.write('rules: {trailing-spaces: disable}')
+
+ with RunContext(self) as ctx:
+ cli.run(('-c', f.name, os.path.join(self.wd, 'a.yaml')))
+ self.assertEqual(ctx.returncode, 0)
+
+ with open(os.path.join(self.wd, 'config'), 'w') as f:
+ f.write('rules: {trailing-spaces: enable}')
+
+ with RunContext(self) as ctx:
+ cli.run(('-c', f.name, os.path.join(self.wd, 'a.yaml')))
+ self.assertEqual(ctx.returncode, 1)
+
+ @unittest.skipIf(os.environ.get('GITHUB_RUN_ID'), '$HOME not overridable')
+ def test_run_with_user_global_config_file(self):
+ home = os.path.join(self.wd, 'fake-home')
+ dir = os.path.join(home, '.config', 'yamllint')
+ os.makedirs(dir)
+ config = os.path.join(dir, 'config')
+
+ self.addCleanup(os.environ.update, HOME=os.environ['HOME'])
+ os.environ['HOME'] = home
+
+ with open(config, 'w') as f:
+ f.write('rules: {trailing-spaces: disable}')
+
+ with RunContext(self) as ctx:
+ cli.run((os.path.join(self.wd, 'a.yaml'), ))
+ self.assertEqual(ctx.returncode, 0)
+
+ with open(config, 'w') as f:
+ f.write('rules: {trailing-spaces: enable}')
+
+ with RunContext(self) as ctx:
+ cli.run((os.path.join(self.wd, 'a.yaml'), ))
+ self.assertEqual(ctx.returncode, 1)
+
+ def test_run_with_user_xdg_config_home_in_env(self):
+ self.addCleanup(os.environ.__delitem__, 'XDG_CONFIG_HOME')
+
+ with tempfile.TemporaryDirectory('w') as d:
+ os.environ['XDG_CONFIG_HOME'] = d
+ os.makedirs(os.path.join(d, 'yamllint'))
+ with open(os.path.join(d, 'yamllint', 'config'), 'w') as f:
+ f.write('extends: relaxed')
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', os.path.join(self.wd, 'warn.yaml')))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ def test_run_with_user_yamllint_config_file_in_env(self):
+ self.addCleanup(os.environ.__delitem__, 'YAMLLINT_CONFIG_FILE')
+
+ with tempfile.NamedTemporaryFile('w') as f:
+ os.environ['YAMLLINT_CONFIG_FILE'] = f.name
+ f.write('rules: {trailing-spaces: disable}')
+ f.flush()
+ with RunContext(self) as ctx:
+ cli.run((os.path.join(self.wd, 'a.yaml'), ))
+ self.assertEqual(ctx.returncode, 0)
+
+ with tempfile.NamedTemporaryFile('w') as f:
+ os.environ['YAMLLINT_CONFIG_FILE'] = f.name
+ f.write('rules: {trailing-spaces: enable}')
+ f.flush()
+ with RunContext(self) as ctx:
+ cli.run((os.path.join(self.wd, 'a.yaml'), ))
+ self.assertEqual(ctx.returncode, 1)
+
+ def test_run_with_locale(self):
+ # check for availability of locale, otherwise skip the test
+ # reset to default before running the test,
+ # as the first two runs don't use setlocale()
+ 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')
+ locale.setlocale(locale.LC_ALL, (None, None))
+
+ # C + en.yaml should fail
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules: { key-ordering: enable }',
+ os.path.join(self.wd, 'en.yaml')))
+ self.assertEqual(ctx.returncode, 1)
+
+ # C + c.yaml should pass
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules: { key-ordering: enable }',
+ os.path.join(self.wd, 'c.yaml')))
+ self.assertEqual(ctx.returncode, 0)
+
+ # the next two runs use setlocale() inside,
+ # so we need to clean up afterwards
+ self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
+
+ # en_US + en.yaml should pass
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'locale: en_US.UTF-8\n'
+ 'rules: { key-ordering: enable }',
+ os.path.join(self.wd, 'en.yaml')))
+ self.assertEqual(ctx.returncode, 0)
+
+ # en_US + c.yaml should fail
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'locale: en_US.UTF-8\n'
+ 'rules: { key-ordering: enable }',
+ os.path.join(self.wd, 'c.yaml')))
+ self.assertEqual(ctx.returncode, 1)
+
+ def test_run_version(self):
+ with RunContext(self) as ctx:
+ cli.run(('--version', ))
+ self.assertEqual(ctx.returncode, 0)
+ self.assertRegex(ctx.stdout + ctx.stderr, r'yamllint \d+\.\d+')
+
+ def test_run_non_existing_file(self):
+ path = os.path.join(self.wd, 'i-do-not-exist.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual(ctx.returncode, -1)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'No such file or directory')
+
+ def test_run_one_problem_file(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual(ctx.returncode, 1)
+ self.assertEqual(ctx.stdout, (
+ f'{path}:2:4: [error] trailing spaces (trailing-spaces)\n'
+ f'{path}:3:4: [error] no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'))
+ self.assertEqual(ctx.stderr, '')
+
+ def test_run_one_warning(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual(ctx.returncode, 0)
+
+ def test_run_warning_in_strict_mode(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', '--strict', path))
+ self.assertEqual(ctx.returncode, 2)
+
+ def test_run_one_ok_file(self):
+ path = os.path.join(self.wd, 'sub', 'ok.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ def test_run_empty_file(self):
+ path = os.path.join(self.wd, 'empty.yml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ @unittest.skipIf(not utf8_available(), 'C.UTF-8 not available')
+ def test_run_non_ascii_file(self):
+ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
+ self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
+
+ path = os.path.join(self.wd, 'non-ascii', 'éçäγλνπ¥', 'utf-8')
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ def test_run_multiple_files(self):
+ items = [os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 's')]
+ path = items[1] + '/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'
+
+ with RunContext(self) as ctx:
+ cli.run(['-f', 'parsable'] + items)
+ self.assertEqual((ctx.returncode, ctx.stderr), (1, ''))
+ self.assertEqual(ctx.stdout, (
+ f'{path}:3:1: [error] duplication of key "key" in mapping '
+ f'(key-duplicates)\n'))
+
+ def test_run_piped_output_nocolor(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, ))
+ self.assertEqual((ctx.returncode, ctx.stderr), (1, ''))
+ self.assertEqual(ctx.stdout, (
+ f'{path}\n'
+ f' 2:4 error trailing spaces (trailing-spaces)\n'
+ f' 3:4 error no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'
+ f'\n'))
+
+ def test_run_default_format_output_in_tty(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ # Create a pseudo-TTY and redirect stdout to it
+ master, slave = pty.openpty()
+ sys.stdout = sys.stderr = os.fdopen(slave, 'w')
+
+ with self.assertRaises(SystemExit) as ctx:
+ cli.run((path, ))
+ sys.stdout.flush()
+
+ self.assertEqual(ctx.exception.code, 1)
+
+ # Read output from TTY
+ output = os.fdopen(master, 'r')
+ flag = fcntl.fcntl(master, fcntl.F_GETFD)
+ fcntl.fcntl(master, fcntl.F_SETFL, flag | os.O_NONBLOCK)
+
+ out = output.read().replace('\r\n', '\n')
+
+ sys.stdout.close()
+ sys.stderr.close()
+ output.close()
+
+ self.assertEqual(out, (
+ f'\033[4m{path}\033[0m\n'
+ f' \033[2m2:4\033[0m \033[31merror\033[0m '
+ f'trailing spaces \033[2m(trailing-spaces)\033[0m\n'
+ f' \033[2m3:4\033[0m \033[31merror\033[0m '
+ f'no new line character at the end of file '
+ f'\033[2m(new-line-at-end-of-file)\033[0m\n'
+ f'\n'))
+
+ def test_run_default_format_output_without_tty(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, ))
+ expected_out = (
+ f'{path}\n'
+ f' 2:4 error trailing spaces (trailing-spaces)\n'
+ f' 3:4 error no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_auto_output_without_tty_output(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'auto'))
+ expected_out = (
+ f'{path}\n'
+ f' 2:4 error trailing spaces (trailing-spaces)\n'
+ f' 3:4 error no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_format_colored(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'colored'))
+ expected_out = (
+ f'\033[4m{path}\033[0m\n'
+ f' \033[2m2:4\033[0m \033[31merror\033[0m '
+ f'trailing spaces \033[2m(trailing-spaces)\033[0m\n'
+ f' \033[2m3:4\033[0m \033[31merror\033[0m '
+ f'no new line character at the end of file '
+ f'\033[2m(new-line-at-end-of-file)\033[0m\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_format_colored_warning(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'colored'))
+ expected_out = (
+ f'\033[4m{path}\033[0m\n'
+ f' \033[2m1:1\033[0m \033[33mwarning\033[0m '
+ f'missing document start "---" \033[2m(document-start)\033[0m\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (0, expected_out, ''))
+
+ def test_run_format_github(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'github'))
+ expected_out = (
+ f'::group::{path}\n'
+ f'::error file={path},line=2,col=4::2:4 [trailing-spaces] trailing'
+ f' spaces\n'
+ f'::error file={path},line=3,col=4::3:4 [new-line-at-end-of-file]'
+ f' no new line character at the end of file\n'
+ f'::endgroup::\n\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_github_actions_detection(self):
+ path = os.path.join(self.wd, 'a.yaml')
+ self.addCleanup(os.environ.__delitem__, 'GITHUB_ACTIONS')
+ self.addCleanup(os.environ.__delitem__, 'GITHUB_WORKFLOW')
+
+ with RunContext(self) as ctx:
+ os.environ['GITHUB_ACTIONS'] = 'something'
+ os.environ['GITHUB_WORKFLOW'] = 'something'
+ cli.run((path, ))
+ expected_out = (
+ f'::group::{path}\n'
+ f'::error file={path},line=2,col=4::2:4 [trailing-spaces] trailing'
+ f' spaces\n'
+ f'::error file={path},line=3,col=4::3:4 [new-line-at-end-of-file]'
+ f' no new line character at the end of file\n'
+ f'::endgroup::\n\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_read_from_stdin(self):
+ # prepares stdin with an invalid yaml string so that we can check
+ # for its specific error, and be assured that stdin was read
+ self.addCleanup(setattr, sys, 'stdin', sys.__stdin__)
+ sys.stdin = StringIO(
+ 'I am a string\n'
+ 'therefore: I am an error\n')
+
+ with RunContext(self) as ctx:
+ cli.run(('-', '-f', 'parsable'))
+ expected_out = (
+ 'stdin:2:10: [error] syntax error: '
+ 'mapping values are not allowed here (syntax)\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_no_warnings(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--no-warnings', '-f', 'auto'))
+ expected_out = (
+ f'{path}\n'
+ f' 2:4 error trailing spaces (trailing-spaces)\n'
+ f' 3:4 error no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--no-warnings', '-f', 'auto'))
+ self.assertEqual(ctx.returncode, 0)
+
+ def test_run_no_warnings_and_strict(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--no-warnings', '-s'))
+ self.assertEqual(ctx.returncode, 2)
+
+ def test_run_non_universal_newline(self):
+ path = os.path.join(self.wd, 'dos.yml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules:\n new-lines:\n type: dos', path))
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules:\n new-lines:\n type: unix', path))
+ expected_out = (
+ f'{path}\n'
+ f' 1:4 error wrong new line character: expected \\n'
+ f' (new-lines)\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_list_files(self):
+ with RunContext(self) as ctx:
+ cli.run(('--list-files', self.wd))
+ self.assertEqual(ctx.returncode, 0)
+ self.assertEqual(
+ sorted(ctx.stdout.splitlines()),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+ config = '{ignore: "*.yml", yaml-files: ["*.*"]}'
+ with RunContext(self) as ctx:
+ cli.run(('--list-files', '-d', config, self.wd))
+ self.assertEqual(ctx.returncode, 0)
+ self.assertEqual(
+ sorted(ctx.stdout.splitlines()),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 'no-yaml.json'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+
+class CommandLineConfigTestCase(unittest.TestCase):
+ def test_config_file(self):
+ workspace = {'a.yml': 'hello: world\n'}
+ conf = ('---\n'
+ 'extends: relaxed\n')
+
+ for conf_file in ('.yamllint', '.yamllint.yml', '.yamllint.yaml'):
+ with self.subTest(conf_file):
+ with temp_workspace(workspace):
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, './a.yml:1:1: [warning] missing document '
+ 'start "---" (document-start)\n', ''))
+
+ with temp_workspace({**workspace, **{conf_file: conf}}):
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, '', ''))
+
+ def test_parent_config_file(self):
+ workspace = {'a/b/c/d/e/f/g/a.yml': 'hello: world\n'}
+ conf = ('---\n'
+ 'extends: relaxed\n')
+
+ for conf_file in ('.yamllint', '.yamllint.yml', '.yamllint.yaml'):
+ with self.subTest(conf_file):
+ with temp_workspace(workspace):
+ with RunContext(self) as ctx:
+ os.chdir('a/b/c/d/e/f')
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, './g/a.yml:1:1: [warning] missing '
+ 'document start "---" (document-start)\n',
+ ''))
+
+ with temp_workspace({**workspace, **{conf_file: conf}}):
+ with RunContext(self) as ctx:
+ os.chdir('a/b/c/d/e/f')
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, '', ''))
+
+ def test_multiple_parent_config_file(self):
+ workspace = {'a/b/c/3spaces.yml': 'array:\n'
+ ' - item\n',
+ 'a/b/c/4spaces.yml': 'array:\n'
+ ' - item\n',
+ 'a/.yamllint': '---\n'
+ 'extends: relaxed\n'
+ 'rules:\n'
+ ' indentation:\n'
+ ' spaces: 4\n',
+ }
+
+ conf3 = ('---\n'
+ 'extends: relaxed\n'
+ 'rules:\n'
+ ' indentation:\n'
+ ' spaces: 3\n')
+
+ with temp_workspace(workspace):
+ with RunContext(self) as ctx:
+ os.chdir('a/b/c')
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, './3spaces.yml:2:4: [warning] wrong indentation: '
+ 'expected 4 but found 3 (indentation)\n', ''))
+
+ with temp_workspace({**workspace, **{'a/b/.yamllint.yml': conf3}}):
+ with RunContext(self) as ctx:
+ os.chdir('a/b/c')
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, './4spaces.yml:2:5: [warning] wrong indentation: '
+ 'expected 3 but found 4 (indentation)\n', ''))
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 0000000..8e90246
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,763 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from io import StringIO
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+from tests.common import build_temp_workspace
+
+from yamllint.config import YamlLintConfigError
+from yamllint import cli
+from yamllint import config
+
+
+class SimpleConfigTestCase(unittest.TestCase):
+ def test_parse_config(self):
+ new = config.YamlLintConfig('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 0\n'
+ ' max-spaces-after: 1\n')
+
+ self.assertEqual(list(new.rules.keys()), ['colons'])
+ self.assertEqual(new.rules['colons']['max-spaces-before'], 0)
+ self.assertEqual(new.rules['colons']['max-spaces-after'], 1)
+
+ self.assertEqual(len(new.enabled_rules(None)), 1)
+
+ def test_invalid_conf(self):
+ with self.assertRaises(config.YamlLintConfigError):
+ config.YamlLintConfig('not: valid: yaml')
+
+ def test_unknown_rule(self):
+ with self.assertRaisesRegex(
+ config.YamlLintConfigError,
+ 'invalid config: no such rule: "this-one-does-not-exist"'):
+ config.YamlLintConfig('rules:\n'
+ ' this-one-does-not-exist: enable\n')
+
+ def test_missing_option(self):
+ c = config.YamlLintConfig('rules:\n'
+ ' colons: enable\n')
+ self.assertEqual(c.rules['colons']['max-spaces-before'], 0)
+ self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
+
+ c = config.YamlLintConfig('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 9\n')
+ self.assertEqual(c.rules['colons']['max-spaces-before'], 9)
+ self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
+
+ def test_unknown_option(self):
+ with self.assertRaisesRegex(
+ config.YamlLintConfigError,
+ 'invalid config: unknown option "abcdef" for rule "colons"'):
+ config.YamlLintConfig('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 0\n'
+ ' max-spaces-after: 1\n'
+ ' abcdef: yes\n')
+
+ def test_yes_no_for_booleans(self):
+ c = config.YamlLintConfig('rules:\n'
+ ' indentation:\n'
+ ' spaces: 2\n'
+ ' indent-sequences: true\n'
+ ' check-multi-line-strings: false\n')
+ self.assertTrue(c.rules['indentation']['indent-sequences'])
+ self.assertEqual(c.rules['indentation']['check-multi-line-strings'],
+ False)
+
+ c = config.YamlLintConfig('rules:\n'
+ ' indentation:\n'
+ ' spaces: 2\n'
+ ' indent-sequences: yes\n'
+ ' check-multi-line-strings: false\n')
+ self.assertTrue(c.rules['indentation']['indent-sequences'])
+ self.assertEqual(c.rules['indentation']['check-multi-line-strings'],
+ False)
+
+ c = config.YamlLintConfig('rules:\n'
+ ' indentation:\n'
+ ' spaces: 2\n'
+ ' indent-sequences: whatever\n'
+ ' check-multi-line-strings: false\n')
+ self.assertEqual(c.rules['indentation']['indent-sequences'],
+ 'whatever')
+ self.assertEqual(c.rules['indentation']['check-multi-line-strings'],
+ False)
+
+ with self.assertRaisesRegex(
+ config.YamlLintConfigError,
+ 'invalid config: option "indent-sequences" of "indentation" '
+ 'should be in '):
+ c = config.YamlLintConfig('rules:\n'
+ ' indentation:\n'
+ ' spaces: 2\n'
+ ' indent-sequences: YES!\n'
+ ' check-multi-line-strings: false\n')
+
+ def test_enable_disable_keywords(self):
+ c = config.YamlLintConfig('rules:\n'
+ ' colons: enable\n'
+ ' hyphens: disable\n')
+ self.assertEqual(c.rules['colons'], {'level': 'error',
+ 'max-spaces-after': 1,
+ 'max-spaces-before': 0})
+ self.assertEqual(c.rules['hyphens'], False)
+
+ def test_validate_rule_conf(self):
+ class Rule:
+ ID = 'fake'
+
+ self.assertFalse(config.validate_rule_conf(Rule, False))
+ self.assertEqual(config.validate_rule_conf(Rule, {}),
+ {'level': 'error'})
+
+ config.validate_rule_conf(Rule, {'level': 'error'})
+ config.validate_rule_conf(Rule, {'level': 'warning'})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule, {'level': 'warn'})
+
+ Rule.CONF = {'length': int}
+ Rule.DEFAULT = {'length': 80}
+ config.validate_rule_conf(Rule, {'length': 8})
+ config.validate_rule_conf(Rule, {})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule, {'height': 8})
+
+ Rule.CONF = {'a': bool, 'b': int}
+ Rule.DEFAULT = {'a': True, 'b': -42}
+ config.validate_rule_conf(Rule, {'a': True, 'b': 0})
+ config.validate_rule_conf(Rule, {'a': True})
+ config.validate_rule_conf(Rule, {'b': 0})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule, {'a': 1, 'b': 0})
+
+ Rule.CONF = {'choice': (True, 88, 'str')}
+ Rule.DEFAULT = {'choice': 88}
+ config.validate_rule_conf(Rule, {'choice': True})
+ config.validate_rule_conf(Rule, {'choice': 88})
+ config.validate_rule_conf(Rule, {'choice': 'str'})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule, {'choice': False})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule, {'choice': 99})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule, {'choice': 'abc'})
+
+ Rule.CONF = {'choice': (int, 'hardcoded')}
+ Rule.DEFAULT = {'choice': 1337}
+ config.validate_rule_conf(Rule, {'choice': 42})
+ config.validate_rule_conf(Rule, {'choice': 'hardcoded'})
+ config.validate_rule_conf(Rule, {})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule, {'choice': False})
+ 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']})
+
+ def test_invalid_rule(self):
+ with self.assertRaisesRegex(
+ config.YamlLintConfigError,
+ 'invalid config: rule "colons": should be either '
+ '"enable", "disable" or a dict'):
+ config.YamlLintConfig('rules:\n'
+ ' colons: invalid\n')
+
+ def test_invalid_ignore(self):
+ with self.assertRaisesRegex(
+ config.YamlLintConfigError,
+ 'invalid config: ignore should contain file patterns'):
+ config.YamlLintConfig('ignore: yes\n')
+
+ def test_invalid_rule_ignore(self):
+ with self.assertRaisesRegex(
+ config.YamlLintConfigError,
+ 'invalid config: ignore should contain file patterns'):
+ config.YamlLintConfig('rules:\n'
+ ' colons:\n'
+ ' ignore: yes\n')
+
+ def test_invalid_locale(self):
+ with self.assertRaisesRegex(
+ config.YamlLintConfigError,
+ 'invalid config: locale should be a string'):
+ config.YamlLintConfig('locale: yes\n')
+
+ def test_invalid_yaml_files(self):
+ with self.assertRaisesRegex(
+ config.YamlLintConfigError,
+ 'invalid config: yaml-files should be a list of file '
+ 'patterns'):
+ config.YamlLintConfig('yaml-files: yes\n')
+
+
+class ExtendedConfigTestCase(unittest.TestCase):
+ def test_extend_on_object(self):
+ old = config.YamlLintConfig('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 0\n'
+ ' max-spaces-after: 1\n')
+ new = config.YamlLintConfig('rules:\n'
+ ' hyphens:\n'
+ ' max-spaces-after: 2\n')
+ new.extend(old)
+
+ self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens'])
+ self.assertEqual(new.rules['colons']['max-spaces-before'], 0)
+ self.assertEqual(new.rules['colons']['max-spaces-after'], 1)
+ self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2)
+
+ self.assertEqual(len(new.enabled_rules(None)), 2)
+
+ def test_extend_on_file(self):
+ with tempfile.NamedTemporaryFile('w') as f:
+ f.write('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 0\n'
+ ' max-spaces-after: 1\n')
+ f.flush()
+ c = config.YamlLintConfig('extends: ' + f.name + '\n'
+ 'rules:\n'
+ ' hyphens:\n'
+ ' max-spaces-after: 2\n')
+
+ self.assertEqual(sorted(c.rules.keys()), ['colons', 'hyphens'])
+ self.assertEqual(c.rules['colons']['max-spaces-before'], 0)
+ self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
+ self.assertEqual(c.rules['hyphens']['max-spaces-after'], 2)
+
+ self.assertEqual(len(c.enabled_rules(None)), 2)
+
+ def test_extend_remove_rule(self):
+ with tempfile.NamedTemporaryFile('w') as f:
+ f.write('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 0\n'
+ ' max-spaces-after: 1\n'
+ ' hyphens:\n'
+ ' max-spaces-after: 2\n')
+ f.flush()
+ c = config.YamlLintConfig('extends: ' + f.name + '\n'
+ 'rules:\n'
+ ' colons: disable\n')
+
+ self.assertEqual(sorted(c.rules.keys()), ['colons', 'hyphens'])
+ self.assertFalse(c.rules['colons'])
+ self.assertEqual(c.rules['hyphens']['max-spaces-after'], 2)
+
+ self.assertEqual(len(c.enabled_rules(None)), 1)
+
+ def test_extend_edit_rule(self):
+ with tempfile.NamedTemporaryFile('w') as f:
+ f.write('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 0\n'
+ ' max-spaces-after: 1\n'
+ ' hyphens:\n'
+ ' max-spaces-after: 2\n')
+ f.flush()
+ c = config.YamlLintConfig('extends: ' + f.name + '\n'
+ 'rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 3\n'
+ ' max-spaces-after: 4\n')
+
+ self.assertEqual(sorted(c.rules.keys()), ['colons', 'hyphens'])
+ self.assertEqual(c.rules['colons']['max-spaces-before'], 3)
+ self.assertEqual(c.rules['colons']['max-spaces-after'], 4)
+ self.assertEqual(c.rules['hyphens']['max-spaces-after'], 2)
+
+ self.assertEqual(len(c.enabled_rules(None)), 2)
+
+ def test_extend_reenable_rule(self):
+ with tempfile.NamedTemporaryFile('w') as f:
+ f.write('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 0\n'
+ ' max-spaces-after: 1\n'
+ ' hyphens: disable\n')
+ f.flush()
+ c = config.YamlLintConfig('extends: ' + f.name + '\n'
+ 'rules:\n'
+ ' hyphens:\n'
+ ' max-spaces-after: 2\n')
+
+ self.assertEqual(sorted(c.rules.keys()), ['colons', 'hyphens'])
+ self.assertEqual(c.rules['colons']['max-spaces-before'], 0)
+ self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
+ self.assertEqual(c.rules['hyphens']['max-spaces-after'], 2)
+
+ self.assertEqual(len(c.enabled_rules(None)), 2)
+
+ def test_extend_recursive_default_values(self):
+ with tempfile.NamedTemporaryFile('w') as f:
+ f.write('rules:\n'
+ ' braces:\n'
+ ' max-spaces-inside: 1248\n')
+ f.flush()
+ c = config.YamlLintConfig('extends: ' + f.name + '\n'
+ 'rules:\n'
+ ' braces:\n'
+ ' min-spaces-inside-empty: 2357\n')
+
+ self.assertEqual(c.rules['braces']['min-spaces-inside'], 0)
+ self.assertEqual(c.rules['braces']['max-spaces-inside'], 1248)
+ self.assertEqual(c.rules['braces']['min-spaces-inside-empty'], 2357)
+ self.assertEqual(c.rules['braces']['max-spaces-inside-empty'], -1)
+
+ with tempfile.NamedTemporaryFile('w') as f:
+ f.write('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 1337\n')
+ f.flush()
+ c = config.YamlLintConfig('extends: ' + f.name + '\n'
+ 'rules:\n'
+ ' colons: enable\n')
+
+ self.assertEqual(c.rules['colons']['max-spaces-before'], 1337)
+ self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
+
+ with tempfile.NamedTemporaryFile('w') as f1, \
+ tempfile.NamedTemporaryFile('w') as f2:
+ f1.write('rules:\n'
+ ' colons:\n'
+ ' max-spaces-before: 1337\n')
+ f1.flush()
+ f2.write('extends: ' + f1.name + '\n'
+ 'rules:\n'
+ ' colons: disable\n')
+ f2.flush()
+ c = config.YamlLintConfig('extends: ' + f2.name + '\n'
+ 'rules:\n'
+ ' colons: enable\n')
+
+ self.assertEqual(c.rules['colons']['max-spaces-before'], 0)
+ self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
+
+ def test_extended_ignore_str(self):
+ with tempfile.NamedTemporaryFile('w') as f:
+ f.write('ignore: |\n'
+ ' *.template.yaml\n')
+ f.flush()
+ c = config.YamlLintConfig('extends: ' + f.name + '\n')
+
+ self.assertEqual(c.ignore.match_file('test.template.yaml'), True)
+ self.assertEqual(c.ignore.match_file('test.yaml'), False)
+
+ def test_extended_ignore_list(self):
+ with tempfile.NamedTemporaryFile('w') as f:
+ f.write('ignore:\n'
+ ' - "*.template.yaml"\n')
+ f.flush()
+ c = config.YamlLintConfig('extends: ' + f.name + '\n')
+
+ self.assertEqual(c.ignore.match_file('test.template.yaml'), True)
+ self.assertEqual(c.ignore.match_file('test.yaml'), False)
+
+
+class ExtendedLibraryConfigTestCase(unittest.TestCase):
+ def test_extend_config_disable_rule(self):
+ old = config.YamlLintConfig('extends: default')
+ new = config.YamlLintConfig('extends: default\n'
+ 'rules:\n'
+ ' trailing-spaces: disable\n')
+
+ old.rules['trailing-spaces'] = False
+
+ self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
+ for rule in new.rules:
+ self.assertEqual(new.rules[rule], old.rules[rule])
+
+ def test_extend_config_override_whole_rule(self):
+ old = config.YamlLintConfig('extends: default')
+ new = config.YamlLintConfig('extends: default\n'
+ 'rules:\n'
+ ' empty-lines:\n'
+ ' max: 42\n'
+ ' max-start: 43\n'
+ ' max-end: 44\n')
+
+ old.rules['empty-lines']['max'] = 42
+ old.rules['empty-lines']['max-start'] = 43
+ old.rules['empty-lines']['max-end'] = 44
+
+ self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
+ for rule in new.rules:
+ self.assertEqual(new.rules[rule], old.rules[rule])
+ self.assertEqual(new.rules['empty-lines']['max'], 42)
+ self.assertEqual(new.rules['empty-lines']['max-start'], 43)
+ self.assertEqual(new.rules['empty-lines']['max-end'], 44)
+
+ def test_extend_config_override_rule_partly(self):
+ old = config.YamlLintConfig('extends: default')
+ new = config.YamlLintConfig('extends: default\n'
+ 'rules:\n'
+ ' empty-lines:\n'
+ ' max-start: 42\n')
+
+ old.rules['empty-lines']['max-start'] = 42
+
+ self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
+ for rule in new.rules:
+ self.assertEqual(new.rules[rule], old.rules[rule])
+ self.assertEqual(new.rules['empty-lines']['max'], 2)
+ self.assertEqual(new.rules['empty-lines']['max-start'], 42)
+ self.assertEqual(new.rules['empty-lines']['max-end'], 0)
+
+
+class IgnoreConfigTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ bad_yaml = ('---\n'
+ '- key: val1\n'
+ ' key: val2\n'
+ '- trailing space \n'
+ '- lonely hyphen\n')
+
+ cls.wd = build_temp_workspace({
+ 'bin/file.lint-me-anyway.yaml': bad_yaml,
+ 'bin/file.yaml': bad_yaml,
+ 'file-at-root.yaml': bad_yaml,
+ 'file.dont-lint-me.yaml': bad_yaml,
+ 'ign-dup/file.yaml': bad_yaml,
+ 'ign-dup/sub/dir/file.yaml': bad_yaml,
+ 'ign-trail/file.yaml': bad_yaml,
+ 'include/ign-dup/sub/dir/file.yaml': bad_yaml,
+ 's/s/ign-trail/file.yaml': bad_yaml,
+ 's/s/ign-trail/s/s/file.yaml': bad_yaml,
+ 's/s/ign-trail/s/s/file2.lint-me-anyway.yaml': bad_yaml,
+ })
+
+ cls.backup_wd = os.getcwd()
+ os.chdir(cls.wd)
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ os.chdir(cls.backup_wd)
+
+ shutil.rmtree(cls.wd)
+
+ def test_mutually_exclusive_ignore_keys(self):
+ self.assertRaises(
+ YamlLintConfigError,
+ config.YamlLintConfig, 'extends: default\n'
+ 'ignore-from-file: .gitignore\n'
+ 'ignore: |\n'
+ ' *.dont-lint-me.yaml\n'
+ ' /bin/\n')
+
+ def test_ignore_from_file_not_exist(self):
+ self.assertRaises(
+ FileNotFoundError,
+ config.YamlLintConfig, 'extends: default\n'
+ 'ignore-from-file: not_found_file\n')
+
+ def test_ignore_from_file_incorrect_type(self):
+ self.assertRaises(
+ YamlLintConfigError,
+ config.YamlLintConfig, 'extends: default\n'
+ 'ignore-from-file: 0\n')
+ self.assertRaises(
+ YamlLintConfigError,
+ config.YamlLintConfig, 'extends: default\n'
+ 'ignore-from-file: [0]\n')
+
+ def test_no_ignore(self):
+ sys.stdout = StringIO()
+ with self.assertRaises(SystemExit):
+ cli.run(('-f', 'parsable', '.'))
+
+ out = sys.stdout.getvalue()
+ out = '\n'.join(sorted(out.splitlines()))
+
+ keydup = '[error] duplication of key "key" in mapping (key-duplicates)'
+ trailing = '[error] trailing spaces (trailing-spaces)'
+ hyphen = '[error] too many spaces after hyphen (hyphens)'
+
+ self.assertEqual(out, '\n'.join((
+ './bin/file.lint-me-anyway.yaml:3:3: ' + keydup,
+ './bin/file.lint-me-anyway.yaml:4:17: ' + trailing,
+ './bin/file.lint-me-anyway.yaml:5:5: ' + hyphen,
+ './bin/file.yaml:3:3: ' + keydup,
+ './bin/file.yaml:4:17: ' + trailing,
+ './bin/file.yaml:5:5: ' + hyphen,
+ './file-at-root.yaml:3:3: ' + keydup,
+ './file-at-root.yaml:4:17: ' + trailing,
+ './file-at-root.yaml:5:5: ' + hyphen,
+ './file.dont-lint-me.yaml:3:3: ' + keydup,
+ './file.dont-lint-me.yaml:4:17: ' + trailing,
+ './file.dont-lint-me.yaml:5:5: ' + hyphen,
+ './ign-dup/file.yaml:3:3: ' + keydup,
+ './ign-dup/file.yaml:4:17: ' + trailing,
+ './ign-dup/file.yaml:5:5: ' + hyphen,
+ './ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
+ './ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './ign-trail/file.yaml:3:3: ' + keydup,
+ './ign-trail/file.yaml:4:17: ' + trailing,
+ './ign-trail/file.yaml:5:5: ' + hyphen,
+ './include/ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
+ './include/ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './include/ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/file.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/s/s/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
+ )))
+
+ def test_run_with_ignore_str(self):
+ with open(os.path.join(self.wd, '.yamllint'), 'w') as f:
+ f.write('extends: default\n'
+ 'ignore: |\n'
+ ' *.dont-lint-me.yaml\n'
+ ' /bin/\n'
+ ' !/bin/*.lint-me-anyway.yaml\n'
+ 'rules:\n'
+ ' key-duplicates:\n'
+ ' ignore: |\n'
+ ' /ign-dup\n'
+ ' trailing-spaces:\n'
+ ' ignore: |\n'
+ ' ign-trail\n'
+ ' !*.lint-me-anyway.yaml\n')
+
+ sys.stdout = StringIO()
+ with self.assertRaises(SystemExit):
+ cli.run(('-f', 'parsable', '.'))
+
+ out = sys.stdout.getvalue()
+ out = '\n'.join(sorted(out.splitlines()))
+
+ docstart = '[warning] missing document start "---" (document-start)'
+ keydup = '[error] duplication of key "key" in mapping (key-duplicates)'
+ trailing = '[error] trailing spaces (trailing-spaces)'
+ hyphen = '[error] too many spaces after hyphen (hyphens)'
+
+ self.assertEqual(out, '\n'.join((
+ './.yamllint:1:1: ' + docstart,
+ './bin/file.lint-me-anyway.yaml:3:3: ' + keydup,
+ './bin/file.lint-me-anyway.yaml:4:17: ' + trailing,
+ './bin/file.lint-me-anyway.yaml:5:5: ' + hyphen,
+ './file-at-root.yaml:3:3: ' + keydup,
+ './file-at-root.yaml:4:17: ' + trailing,
+ './file-at-root.yaml:5:5: ' + hyphen,
+ './ign-dup/file.yaml:4:17: ' + trailing,
+ './ign-dup/file.yaml:5:5: ' + hyphen,
+ './ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './ign-trail/file.yaml:3:3: ' + keydup,
+ './ign-trail/file.yaml:5:5: ' + hyphen,
+ './include/ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
+ './include/ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './include/ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
+ )))
+
+ def test_run_with_ignore_list(self):
+ with open(os.path.join(self.wd, '.yamllint'), 'w') as f:
+ f.write('extends: default\n'
+ 'ignore:\n'
+ ' - "*.dont-lint-me.yaml"\n'
+ ' - "/bin/"\n'
+ ' - "!/bin/*.lint-me-anyway.yaml"\n'
+ 'rules:\n'
+ ' key-duplicates:\n'
+ ' ignore:\n'
+ ' - "/ign-dup"\n'
+ ' trailing-spaces:\n'
+ ' ignore:\n'
+ ' - "ign-trail"\n'
+ ' - "!*.lint-me-anyway.yaml"\n')
+
+ sys.stdout = StringIO()
+ with self.assertRaises(SystemExit):
+ cli.run(('-f', 'parsable', '.'))
+
+ out = sys.stdout.getvalue()
+ out = '\n'.join(sorted(out.splitlines()))
+
+ docstart = '[warning] missing document start "---" (document-start)'
+ keydup = '[error] duplication of key "key" in mapping (key-duplicates)'
+ trailing = '[error] trailing spaces (trailing-spaces)'
+ hyphen = '[error] too many spaces after hyphen (hyphens)'
+
+ self.assertEqual(out, '\n'.join((
+ './.yamllint:1:1: ' + docstart,
+ './bin/file.lint-me-anyway.yaml:3:3: ' + keydup,
+ './bin/file.lint-me-anyway.yaml:4:17: ' + trailing,
+ './bin/file.lint-me-anyway.yaml:5:5: ' + hyphen,
+ './file-at-root.yaml:3:3: ' + keydup,
+ './file-at-root.yaml:4:17: ' + trailing,
+ './file-at-root.yaml:5:5: ' + hyphen,
+ './ign-dup/file.yaml:4:17: ' + trailing,
+ './ign-dup/file.yaml:5:5: ' + hyphen,
+ './ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './ign-trail/file.yaml:3:3: ' + keydup,
+ './ign-trail/file.yaml:5:5: ' + hyphen,
+ './include/ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
+ './include/ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './include/ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
+ )))
+
+ def test_run_with_ignore_from_file(self):
+ with open(os.path.join(self.wd, '.yamllint'), 'w') as f:
+ f.write('extends: default\n'
+ 'ignore-from-file: .gitignore\n')
+ with open(os.path.join(self.wd, '.gitignore'), 'w') as f:
+ f.write('*.dont-lint-me.yaml\n'
+ '/bin/\n'
+ '!/bin/*.lint-me-anyway.yaml\n')
+
+ sys.stdout = StringIO()
+ with self.assertRaises(SystemExit):
+ cli.run(('-f', 'parsable', '.'))
+
+ out = sys.stdout.getvalue()
+ out = '\n'.join(sorted(out.splitlines()))
+
+ docstart = '[warning] missing document start "---" (document-start)'
+ keydup = '[error] duplication of key "key" in mapping (key-duplicates)'
+ trailing = '[error] trailing spaces (trailing-spaces)'
+ hyphen = '[error] too many spaces after hyphen (hyphens)'
+
+ self.assertEqual(out, '\n'.join((
+ './.yamllint:1:1: ' + docstart,
+ './bin/file.lint-me-anyway.yaml:3:3: ' + keydup,
+ './bin/file.lint-me-anyway.yaml:4:17: ' + trailing,
+ './bin/file.lint-me-anyway.yaml:5:5: ' + hyphen,
+ './file-at-root.yaml:3:3: ' + keydup,
+ './file-at-root.yaml:4:17: ' + trailing,
+ './file-at-root.yaml:5:5: ' + hyphen,
+ './ign-dup/file.yaml:3:3: ' + keydup,
+ './ign-dup/file.yaml:4:17: ' + trailing,
+ './ign-dup/file.yaml:5:5: ' + hyphen,
+ './ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
+ './ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './ign-trail/file.yaml:3:3: ' + keydup,
+ './ign-trail/file.yaml:4:17: ' + trailing,
+ './ign-trail/file.yaml:5:5: ' + hyphen,
+ './include/ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
+ './include/ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './include/ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/file.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/s/s/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
+ )))
+
+ def test_run_with_ignored_from_file(self):
+ with open(os.path.join(self.wd, '.yamllint'), 'w') as f:
+ f.write('ignore-from-file: [.gitignore, .yamlignore]\n'
+ 'extends: default\n')
+ with open(os.path.join(self.wd, '.gitignore'), 'w') as f:
+ f.write('*.dont-lint-me.yaml\n'
+ '/bin/\n')
+ with open(os.path.join(self.wd, '.yamlignore'), 'w') as f:
+ f.write('!/bin/*.lint-me-anyway.yaml\n')
+
+ sys.stdout = StringIO()
+ with self.assertRaises(SystemExit):
+ cli.run(('-f', 'parsable', '.'))
+
+ out = sys.stdout.getvalue()
+ out = '\n'.join(sorted(out.splitlines()))
+
+ docstart = '[warning] missing document start "---" (document-start)'
+ keydup = '[error] duplication of key "key" in mapping (key-duplicates)'
+ trailing = '[error] trailing spaces (trailing-spaces)'
+ hyphen = '[error] too many spaces after hyphen (hyphens)'
+
+ self.assertEqual(out, '\n'.join((
+ './.yamllint:1:1: ' + docstart,
+ './bin/file.lint-me-anyway.yaml:3:3: ' + keydup,
+ './bin/file.lint-me-anyway.yaml:4:17: ' + trailing,
+ './bin/file.lint-me-anyway.yaml:5:5: ' + hyphen,
+ './file-at-root.yaml:3:3: ' + keydup,
+ './file-at-root.yaml:4:17: ' + trailing,
+ './file-at-root.yaml:5:5: ' + hyphen,
+ './ign-dup/file.yaml:3:3: ' + keydup,
+ './ign-dup/file.yaml:4:17: ' + trailing,
+ './ign-dup/file.yaml:5:5: ' + hyphen,
+ './ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
+ './ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './ign-trail/file.yaml:3:3: ' + keydup,
+ './ign-trail/file.yaml:4:17: ' + trailing,
+ './ign-trail/file.yaml:5:5: ' + hyphen,
+ './include/ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
+ './include/ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './include/ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/file.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/s/s/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
+ )))
diff --git a/tests/test_linter.py b/tests/test_linter.py
new file mode 100644
index 0000000..9855120
--- /dev/null
+++ b/tests/test_linter.py
@@ -0,0 +1,66 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import io
+import unittest
+
+from yamllint.config import YamlLintConfig
+from yamllint import linter
+
+
+class LinterTestCase(unittest.TestCase):
+ def fake_config(self):
+ return YamlLintConfig('extends: default')
+
+ def test_run_on_string(self):
+ linter.run('test: document', self.fake_config())
+
+ def test_run_on_bytes(self):
+ linter.run(b'test: document', self.fake_config())
+
+ def test_run_on_unicode(self):
+ linter.run('test: document', self.fake_config())
+
+ def test_run_on_stream(self):
+ linter.run(io.StringIO('hello'), self.fake_config())
+
+ def test_run_on_int(self):
+ self.assertRaises(TypeError, linter.run, 42, self.fake_config())
+
+ def test_run_on_list(self):
+ self.assertRaises(TypeError, linter.run,
+ ['h', 'e', 'l', 'l', 'o'], self.fake_config())
+
+ def test_run_on_non_ascii_chars(self):
+ s = ('- hétérogénéité\n'
+ '# 19.99 €\n')
+ linter.run(s, self.fake_config())
+ linter.run(s.encode('utf-8'), self.fake_config())
+ linter.run(s.encode('iso-8859-15'), self.fake_config())
+
+ s = ('- お早う御座います。\n'
+ '# الأَبْجَدِيَّة العَرَبِيَّة\n')
+ linter.run(s, self.fake_config())
+ linter.run(s.encode('utf-8'), self.fake_config())
+
+ def test_linter_problem_repr_without_rule(self):
+ problem = linter.LintProblem(1, 2, 'problem')
+
+ self.assertEqual(str(problem), '1:2: problem')
+
+ def test_linter_problem_repr_with_rule(self):
+ problem = linter.LintProblem(1, 2, 'problem', 'rule-id')
+
+ self.assertEqual(str(problem), '1:2: problem (rule-id)')
diff --git a/tests/test_module.py b/tests/test_module.py
new file mode 100644
index 0000000..299e153
--- /dev/null
+++ b/tests/test_module.py
@@ -0,0 +1,84 @@
+# Copyright (C) 2017 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 <http://www.gnu.org/licenses/>.
+
+import os
+import shutil
+import subprocess
+import tempfile
+import sys
+import unittest
+
+
+PYTHON = sys.executable or 'python'
+
+
+class ModuleTestCase(unittest.TestCase):
+ def setUp(self):
+ self.wd = tempfile.mkdtemp(prefix='yamllint-tests-')
+
+ # file with only one warning
+ with open(os.path.join(self.wd, 'warn.yaml'), 'w') as f:
+ f.write('key: value\n')
+
+ # file in dir
+ os.mkdir(os.path.join(self.wd, 'sub'))
+ with open(os.path.join(self.wd, 'sub', 'nok.yaml'), 'w') as f:
+ f.write('---\n'
+ 'list: [ 1, 1, 2, 3, 5, 8] \n')
+
+ def tearDown(self):
+ shutil.rmtree(self.wd)
+
+ def test_run_module_no_args(self):
+ with self.assertRaises(subprocess.CalledProcessError) as ctx:
+ subprocess.check_output([PYTHON, '-m', 'yamllint'],
+ stderr=subprocess.STDOUT)
+ self.assertEqual(ctx.exception.returncode, 2)
+ self.assertRegex(ctx.exception.output.decode(), r'^usage: yamllint')
+
+ def test_run_module_on_bad_dir(self):
+ with self.assertRaises(subprocess.CalledProcessError) as ctx:
+ subprocess.check_output([PYTHON, '-m', 'yamllint',
+ '/does/not/exist'],
+ stderr=subprocess.STDOUT)
+ self.assertRegex(ctx.exception.output.decode(),
+ r'No such file or directory')
+
+ def test_run_module_on_file(self):
+ out = subprocess.check_output(
+ [PYTHON, '-m', 'yamllint', os.path.join(self.wd, 'warn.yaml')])
+ lines = out.decode().splitlines()
+ self.assertIn('/warn.yaml', lines[0])
+ self.assertEqual('\n'.join(lines[1:]),
+ ' 1:1 warning missing document start "---"'
+ ' (document-start)\n')
+
+ def test_run_module_on_dir(self):
+ with self.assertRaises(subprocess.CalledProcessError) as ctx:
+ subprocess.check_output([PYTHON, '-m', 'yamllint', self.wd])
+ self.assertEqual(ctx.exception.returncode, 1)
+
+ files = ctx.exception.output.decode().split('\n\n')
+ self.assertIn(
+ '/warn.yaml\n'
+ ' 1:1 warning missing document start "---"'
+ ' (document-start)',
+ files[0])
+ self.assertIn(
+ '/sub/nok.yaml\n'
+ ' 2:9 error too many spaces inside brackets'
+ ' (brackets)\n'
+ ' 2:27 error trailing spaces (trailing-spaces)',
+ files[1])
diff --git a/tests/test_parser.py b/tests/test_parser.py
new file mode 100644
index 0000000..dbeb36b
--- /dev/null
+++ b/tests/test_parser.py
@@ -0,0 +1,152 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import unittest
+
+import yaml
+
+from yamllint.parser import (line_generator, token_or_comment_generator,
+ token_or_comment_or_line_generator,
+ Line, Token, Comment)
+
+
+class ParserTestCase(unittest.TestCase):
+ def test_line_generator(self):
+ e = list(line_generator(''))
+ self.assertEqual(len(e), 1)
+ self.assertEqual(e[0].line_no, 1)
+ self.assertEqual(e[0].start, 0)
+ self.assertEqual(e[0].end, 0)
+
+ e = list(line_generator('\n'))
+ self.assertEqual(len(e), 2)
+
+ e = list(line_generator(' \n'))
+ self.assertEqual(len(e), 2)
+ self.assertEqual(e[0].line_no, 1)
+ self.assertEqual(e[0].start, 0)
+ self.assertEqual(e[0].end, 1)
+
+ e = list(line_generator('\n\n'))
+ self.assertEqual(len(e), 3)
+
+ e = list(line_generator('---\n'
+ 'this is line 1\n'
+ 'line 2\n'
+ '\n'
+ '3\n'))
+ self.assertEqual(len(e), 6)
+ self.assertEqual(e[0].line_no, 1)
+ self.assertEqual(e[0].content, '---')
+ self.assertEqual(e[2].content, 'line 2')
+ self.assertEqual(e[3].content, '')
+ self.assertEqual(e[5].line_no, 6)
+
+ e = list(line_generator('test with\n'
+ 'no newline\n'
+ 'at the end'))
+ self.assertEqual(len(e), 3)
+ self.assertEqual(e[2].line_no, 3)
+ self.assertEqual(e[2].content, 'at the end')
+
+ def test_token_or_comment_generator(self):
+ e = list(token_or_comment_generator(''))
+ self.assertEqual(len(e), 2)
+ self.assertIsNone(e[0].prev)
+ self.assertIsInstance(e[0].curr, yaml.Token)
+ self.assertIsInstance(e[0].next, yaml.Token)
+ self.assertEqual(e[1].prev, e[0].curr)
+ self.assertEqual(e[1].curr, e[0].next)
+ self.assertIsNone(e[1].next)
+
+ e = list(token_or_comment_generator('---\n'
+ 'k: v\n'))
+ self.assertEqual(len(e), 9)
+ self.assertIsInstance(e[3].curr, yaml.KeyToken)
+ self.assertIsInstance(e[5].curr, yaml.ValueToken)
+
+ e = list(token_or_comment_generator('# start comment\n'
+ '- a\n'
+ '- key: val # key=val\n'
+ '# this is\n'
+ '# a block \n'
+ '# comment\n'
+ '- c\n'
+ '# end comment\n'))
+ self.assertEqual(len(e), 21)
+ self.assertIsInstance(e[1], Comment)
+ self.assertEqual(e[1], Comment(1, 1, '# start comment', 0))
+ self.assertEqual(e[11], Comment(3, 13, '# key=val', 0))
+ self.assertEqual(e[12], Comment(4, 1, '# this is', 0))
+ self.assertEqual(e[13], Comment(5, 1, '# a block ', 0))
+ self.assertEqual(e[14], Comment(6, 1, '# comment', 0))
+ self.assertEqual(e[18], Comment(8, 1, '# end comment', 0))
+
+ e = list(token_or_comment_generator('---\n'
+ '# no newline char'))
+ self.assertEqual(e[2], Comment(2, 1, '# no newline char', 0))
+
+ e = list(token_or_comment_generator('# just comment'))
+ self.assertEqual(e[1], Comment(1, 1, '# just comment', 0))
+
+ e = list(token_or_comment_generator('\n'
+ ' # indented comment\n'))
+ self.assertEqual(e[1], Comment(2, 4, '# indented comment', 0))
+
+ e = list(token_or_comment_generator('\n'
+ '# trailing spaces \n'))
+ self.assertEqual(e[1], Comment(2, 1, '# trailing spaces ', 0))
+
+ e = [c for c in
+ token_or_comment_generator('# block\n'
+ '# comment\n'
+ '- data # inline comment\n'
+ '# block\n'
+ '# comment\n'
+ '- k: v # inline comment\n'
+ '- [ l, ist\n'
+ '] # inline comment\n'
+ '- { m: ap\n'
+ '} # inline comment\n'
+ '# block comment\n'
+ '- data # inline comment\n')
+ if isinstance(c, Comment)]
+ self.assertEqual(len(e), 10)
+ self.assertFalse(e[0].is_inline())
+ self.assertFalse(e[1].is_inline())
+ self.assertTrue(e[2].is_inline())
+ self.assertFalse(e[3].is_inline())
+ self.assertFalse(e[4].is_inline())
+ self.assertTrue(e[5].is_inline())
+ self.assertTrue(e[6].is_inline())
+ self.assertTrue(e[7].is_inline())
+ self.assertFalse(e[8].is_inline())
+ self.assertTrue(e[9].is_inline())
+
+ def test_token_or_comment_or_line_generator(self):
+ e = list(token_or_comment_or_line_generator('---\n'
+ 'k: v # k=v\n'))
+ self.assertEqual(len(e), 13)
+ self.assertIsInstance(e[0], Token)
+ self.assertIsInstance(e[0].curr, yaml.StreamStartToken)
+ self.assertIsInstance(e[1], Token)
+ self.assertIsInstance(e[1].curr, yaml.DocumentStartToken)
+ self.assertIsInstance(e[2], Line)
+ self.assertIsInstance(e[3].curr, yaml.BlockMappingStartToken)
+ self.assertIsInstance(e[4].curr, yaml.KeyToken)
+ self.assertIsInstance(e[6].curr, yaml.ValueToken)
+ self.assertIsInstance(e[8], Comment)
+ self.assertIsInstance(e[9], Line)
+ self.assertIsInstance(e[12], Line)
diff --git a/tests/test_spec_examples.py b/tests/test_spec_examples.py
new file mode 100644
index 0000000..ac68e12
--- /dev/null
+++ b/tests/test_spec_examples.py
@@ -0,0 +1,188 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import os
+
+from tests.common import RuleTestCase
+
+
+# This file checks examples from YAML 1.2 specification [1] against yamllint.
+#
+# [1]: http://www.yaml.org/spec/1.2/spec.html
+#
+# Example files generated with:
+#
+# from bs4 import BeautifulSoup
+# with open('spec.html', encoding='iso-8859-1') as f:
+# soup = BeautifulSoup(f, 'lxml')
+# for ex in soup.find_all('div', class_='example'):
+# title = ex.find('p', class_='title').find('b').get_text()
+# id = '-'.join(title.split('\xa0')[:2])[:-1].lower()
+# span = ex.find('span', class_='database')
+# for br in span.find_all("br"):
+# br.replace_with("\n")
+# text = text.replace('\u2193', '') # downwards arrow
+# text = text.replace('\u21d3', '') # double downwards arrow
+# text = text.replace('\u00b7', ' ') # visible space
+# text = text.replace('\u21d4', '') # byte order mark
+# text = text.replace('\u2192', '\t') # right arrow
+# text = text.replace('\u00b0', '') # empty scalar
+# with open(f'tests/yaml-1.2-spec-examples/{id}', 'w',
+# encoding='utf-8') as g:
+# g.write(text)
+
+class SpecificationTestCase(RuleTestCase):
+ rule_id = None
+
+
+conf_general = ('document-start: disable\n'
+ 'comments: {min-spaces-from-content: 1}\n'
+ 'braces: {min-spaces-inside: 1, max-spaces-inside: 1}\n'
+ 'brackets: {min-spaces-inside: 1, max-spaces-inside: 1}\n')
+conf_overrides = {
+ 'example-2.2': 'colons: {max-spaces-after: 2}\n',
+ 'example-2.4': 'colons: {max-spaces-after: 3}\n',
+ 'example-2.5': ('empty-lines: {max-end: 2}\n'
+ 'brackets: {min-spaces-inside: 0, max-spaces-inside: 2}\n'
+ 'commas: {max-spaces-before: -1}\n'),
+ 'example-2.6': ('braces: {min-spaces-inside: 0, max-spaces-inside: 0}\n'
+ 'indentation: disable\n'),
+ 'example-2.12': ('empty-lines: {max-end: 1}\n'
+ 'colons: {max-spaces-before: -1}\n'),
+ 'example-2.16': 'empty-lines: {max-end: 1}\n',
+ 'example-2.18': 'empty-lines: {max-end: 1}\n',
+ 'example-2.19': 'empty-lines: {max-end: 1}\n',
+ 'example-2.28': 'empty-lines: {max-end: 3}\n',
+ 'example-5.3': ('indentation: {indent-sequences: false}\n'
+ 'colons: {max-spaces-before: 1}\n'),
+ 'example-6.4': 'trailing-spaces: disable\n',
+ 'example-6.5': 'trailing-spaces: disable\n',
+ 'example-6.6': 'trailing-spaces: disable\n',
+ 'example-6.7': 'trailing-spaces: disable\n',
+ 'example-6.8': 'trailing-spaces: disable\n',
+ 'example-6.10': ('empty-lines: {max-end: 2}\n'
+ 'trailing-spaces: disable\n'
+ 'comments-indentation: disable\n'),
+ 'example-6.11': ('empty-lines: {max-end: 1}\n'
+ 'comments-indentation: disable\n'),
+ 'example-6.13': 'comments-indentation: disable\n',
+ 'example-6.14': 'comments-indentation: disable\n',
+ 'example-6.23': 'colons: {max-spaces-before: 1}\n',
+ 'example-7.4': ('colons: {max-spaces-before: 1}\n'
+ 'indentation: disable\n'),
+ 'example-7.5': 'trailing-spaces: disable\n',
+ 'example-7.6': 'trailing-spaces: disable\n',
+ 'example-7.7': 'indentation: disable\n',
+ 'example-7.8': ('colons: {max-spaces-before: 1}\n'
+ 'indentation: disable\n'),
+ 'example-7.9': 'trailing-spaces: disable\n',
+ 'example-7.11': ('colons: {max-spaces-before: 1}\n'
+ 'indentation: disable\n'),
+ 'example-7.13': ('brackets: {min-spaces-inside: 0, max-spaces-inside: 1}\n'
+ 'commas: {max-spaces-before: 1, min-spaces-after: 0}\n'),
+ 'example-7.14': 'indentation: disable\n',
+ 'example-7.15': ('braces: {min-spaces-inside: 0, max-spaces-inside: 1}\n'
+ 'commas: {max-spaces-before: 1, min-spaces-after: 0}\n'
+ 'colons: {max-spaces-before: 1}\n'),
+ 'example-7.16': 'indentation: disable\n',
+ 'example-7.17': 'indentation: disable\n',
+ 'example-7.18': 'indentation: disable\n',
+ 'example-7.19': 'indentation: disable\n',
+ 'example-7.20': ('colons: {max-spaces-before: 1}\n'
+ 'indentation: disable\n'),
+ 'example-8.1': 'empty-lines: {max-end: 1}\n',
+ 'example-8.2': 'trailing-spaces: disable\n',
+ 'example-8.5': ('comments-indentation: disable\n'
+ 'trailing-spaces: disable\n'),
+ 'example-8.6': 'empty-lines: {max-end: 1}\n',
+ 'example-8.7': 'empty-lines: {max-end: 1}\n',
+ 'example-8.8': 'trailing-spaces: disable\n',
+ 'example-8.9': 'empty-lines: {max-end: 1}\n',
+ 'example-8.14': 'colons: {max-spaces-before: 1}\n',
+ 'example-8.16': 'indentation: {spaces: 1}\n',
+ 'example-8.17': 'indentation: disable\n',
+ 'example-8.20': ('indentation: {indent-sequences: false}\n'
+ 'colons: {max-spaces-before: 1}\n'),
+ 'example-8.22': 'indentation: disable\n',
+ 'example-10.1': 'colons: {max-spaces-before: 2}\n',
+ 'example-10.2': 'indentation: {indent-sequences: false}\n',
+ 'example-10.8': 'truthy: disable\n',
+ 'example-10.9': 'truthy: disable\n',
+}
+
+files = os.listdir(os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ 'yaml-1.2-spec-examples'))
+assert len(files) == 132
+
+
+def _gen_test(buffer, conf):
+ def test(self):
+ self.check(buffer, conf)
+ return test
+
+
+# The following tests are blacklisted (i.e. will not be checked against
+# yamllint), because pyyaml is currently not able to parse the contents
+# (using yaml.parse()).
+pyyaml_blacklist = (
+ 'example-2.11',
+ 'example-2.23',
+ 'example-2.24',
+ 'example-2.27',
+ 'example-5.10',
+ 'example-5.12',
+ 'example-5.13',
+ 'example-5.14',
+ 'example-5.6',
+ 'example-6.1',
+ 'example-6.12',
+ 'example-6.15',
+ 'example-6.17',
+ 'example-6.18',
+ 'example-6.19',
+ 'example-6.2',
+ 'example-6.20',
+ 'example-6.21',
+ 'example-6.22',
+ 'example-6.24',
+ 'example-6.25',
+ 'example-6.26',
+ 'example-6.27',
+ 'example-6.3',
+ 'example-7.1',
+ 'example-7.10',
+ 'example-7.12',
+ 'example-7.17',
+ 'example-7.2',
+ 'example-7.21',
+ 'example-7.22',
+ 'example-7.3',
+ 'example-8.18',
+ 'example-8.19',
+ 'example-8.21',
+ 'example-8.3',
+ 'example-9.3',
+ 'example-9.4',
+ 'example-9.5',
+)
+
+for file in files:
+ if file in pyyaml_blacklist:
+ continue
+
+ with open('tests/yaml-1.2-spec-examples/' + file, encoding='utf-8') as f:
+ conf = conf_general + conf_overrides.get(file, '')
+ setattr(SpecificationTestCase, 'test_' + file,
+ _gen_test(f.read(), conf))
diff --git a/tests/test_syntax_errors.py b/tests/test_syntax_errors.py
new file mode 100644
index 0000000..507ab5a
--- /dev/null
+++ b/tests/test_syntax_errors.py
@@ -0,0 +1,93 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class YamlLintTestCase(RuleTestCase):
+ rule_id = None # syntax error
+
+ def test_syntax_errors(self):
+ self.check('---\n'
+ 'this is not: valid: YAML\n', None, problem=(2, 19))
+ self.check('---\n'
+ 'this is: valid YAML\n'
+ '\n'
+ 'this is an error: [\n'
+ '\n'
+ '...\n', None, problem=(6, 1))
+ self.check('%YAML 1.2\n'
+ '%TAG ! tag:clarkevans.com,2002:\n'
+ 'doc: ument\n'
+ '...\n', None, problem=(3, 1))
+
+ def test_empty_flows(self):
+ self.check('---\n'
+ '- []\n'
+ '- {}\n'
+ '- [\n'
+ ']\n'
+ '- {\n'
+ '}\n'
+ '...\n', None)
+
+ def test_explicit_mapping(self):
+ self.check('---\n'
+ '? key\n'
+ ': - value 1\n'
+ ' - value 2\n'
+ '...\n', None)
+ self.check('---\n'
+ '?\n'
+ ' key\n'
+ ': {a: 1}\n'
+ '...\n', None)
+ self.check('---\n'
+ '?\n'
+ ' key\n'
+ ':\n'
+ ' val\n'
+ '...\n', None)
+
+ def test_mapping_between_sequences(self):
+ # This is valid YAML. See http://www.yaml.org/spec/1.2/spec.html,
+ # example 2.11
+ self.check('---\n'
+ '? - Detroit Tigers\n'
+ ' - Chicago cubs\n'
+ ':\n'
+ ' - 2001-07-23\n'
+ '\n'
+ '? [New York Yankees,\n'
+ ' Atlanta Braves]\n'
+ ': [2001-07-02, 2001-08-12,\n'
+ ' 2001-08-14]\n', None)
+
+ def test_sets(self):
+ self.check('---\n'
+ '? key one\n'
+ '? key two\n'
+ '? [non, scalar, key]\n'
+ '? key with value\n'
+ ': value\n'
+ '...\n', None)
+ self.check('---\n'
+ '? - multi\n'
+ ' - line\n'
+ ' - keys\n'
+ '? in:\n'
+ ' a:\n'
+ ' set\n'
+ '...\n', None)
diff --git a/tests/test_yamllint_directives.py b/tests/test_yamllint_directives.py
new file mode 100644
index 0000000..c138144
--- /dev/null
+++ b/tests/test_yamllint_directives.py
@@ -0,0 +1,432 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from tests.common import RuleTestCase
+
+
+class YamllintDirectivesTestCase(RuleTestCase):
+ conf = ('commas: disable\n'
+ 'trailing-spaces: {}\n'
+ 'colons: {max-spaces-before: 1}\n')
+
+ def test_disable_directive(self):
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(4, 8, 'colons'),
+ problem3=(6, 7, 'colons'),
+ problem4=(6, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '# yamllint disable\n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem=(3, 18, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '# yamllint disable\n'
+ '- trailing spaces \n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '# yamllint enable\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(8, 7, 'colons'),
+ problem2=(8, 26, 'trailing-spaces'))
+
+ def test_disable_directive_with_rules(self):
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '# yamllint disable rule:trailing-spaces\n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(5, 8, 'colons'),
+ problem3=(7, 7, 'colons'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '# yamllint disable rule:trailing-spaces\n'
+ '- trailing spaces \n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '# yamllint enable rule:trailing-spaces\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(5, 8, 'colons'),
+ problem2=(8, 7, 'colons'),
+ problem3=(8, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '# yamllint disable rule:trailing-spaces\n'
+ '- trailing spaces \n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '# yamllint enable\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(5, 8, 'colons'),
+ problem2=(8, 7, 'colons'),
+ problem3=(8, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '# yamllint disable\n'
+ '- trailing spaces \n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '# yamllint enable rule:trailing-spaces\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem=(8, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '# yamllint disable rule:colons\n'
+ '- trailing spaces \n'
+ '# yamllint disable rule:trailing-spaces\n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '# yamllint enable rule:colons\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(4, 18, 'trailing-spaces'),
+ problem2=(9, 7, 'colons'))
+
+ def test_disable_line_directive(self):
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '# yamllint disable-line\n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(7, 7, 'colons'),
+ problem3=(7, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '- bad : colon # yamllint disable-line\n'
+ '- [valid , YAML]\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(6, 7, 'colons'),
+ problem3=(6, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '- bad : colon\n'
+ '- [valid , YAML] # yamllint disable-line\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(4, 8, 'colons'),
+ problem3=(6, 7, 'colons'),
+ problem4=(6, 26, 'trailing-spaces'))
+
+ def test_disable_line_directive_with_rules(self):
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '# yamllint disable-line rule:colons\n'
+ '- trailing spaces \n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(4, 18, 'trailing-spaces'),
+ problem2=(5, 8, 'colons'),
+ problem3=(7, 7, 'colons'),
+ problem4=(7, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces # yamllint disable-line rule:colons \n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 55, 'trailing-spaces'),
+ problem2=(4, 8, 'colons'),
+ problem3=(6, 7, 'colons'),
+ problem4=(6, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '# yamllint disable-line rule:colons\n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(7, 7, 'colons'),
+ problem3=(7, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '- bad : colon # yamllint disable-line rule:colons\n'
+ '- [valid , YAML]\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(6, 7, 'colons'),
+ problem3=(6, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '# yamllint disable-line rule:colons\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(4, 8, 'colons'),
+ problem3=(7, 26, 'trailing-spaces'))
+ self.check('---\n'
+ '- [valid , YAML]\n'
+ '- trailing spaces \n'
+ '- bad : colon\n'
+ '- [valid , YAML]\n'
+ '# yamllint disable-line rule:colons rule:trailing-spaces\n'
+ '- bad : colon and spaces \n'
+ '- [valid , YAML]\n',
+ self.conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(4, 8, 'colons'))
+
+ def test_disable_directive_with_rules_and_dos_lines(self):
+ conf = self.conf + 'new-lines: {type: dos}\n'
+ self.check('---\r\n'
+ '- [valid , YAML]\r\n'
+ '# yamllint disable rule:trailing-spaces\r\n'
+ '- trailing spaces \r\n'
+ '- bad : colon\r\n'
+ '- [valid , YAML]\r\n'
+ '# yamllint enable rule:trailing-spaces\r\n'
+ '- bad : colon and spaces \r\n'
+ '- [valid , YAML]\r\n',
+ conf,
+ problem1=(5, 8, 'colons'),
+ problem2=(8, 7, 'colons'),
+ problem3=(8, 26, 'trailing-spaces'))
+ self.check('---\r\n'
+ '- [valid , YAML]\r\n'
+ '- trailing spaces \r\n'
+ '- bad : colon\r\n'
+ '- [valid , YAML]\r\n'
+ '# yamllint disable-line rule:colons\r\n'
+ '- bad : colon and spaces \r\n'
+ '- [valid , YAML]\r\n',
+ conf,
+ problem1=(3, 18, 'trailing-spaces'),
+ problem2=(4, 8, 'colons'),
+ problem3=(7, 26, 'trailing-spaces'))
+
+ def test_directive_on_last_line(self):
+ conf = 'new-line-at-end-of-file: {}'
+ self.check('---\n'
+ 'no new line',
+ conf,
+ problem=(2, 12, 'new-line-at-end-of-file'))
+ self.check('---\n'
+ '# yamllint disable\n'
+ 'no new line',
+ conf)
+ self.check('---\n'
+ 'no new line # yamllint disable',
+ conf)
+
+ def test_indented_directive(self):
+ conf = 'brackets: {min-spaces-inside: 0, max-spaces-inside: 0}'
+ self.check('---\n'
+ '- a: 1\n'
+ ' b:\n'
+ ' c: [ x]\n',
+ conf,
+ problem=(4, 12, 'brackets'))
+ self.check('---\n'
+ '- a: 1\n'
+ ' b:\n'
+ ' # yamllint disable-line rule:brackets\n'
+ ' c: [ x]\n',
+ conf)
+
+ def test_directive_on_itself(self):
+ conf = ('comments: {min-spaces-from-content: 2}\n'
+ 'comments-indentation: {}\n')
+ self.check('---\n'
+ '- a: 1 # comment too close\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf,
+ problem1=(2, 8, 'comments'),
+ problem2=(4, 2, 'comments-indentation'))
+ self.check('---\n'
+ '# yamllint disable\n'
+ '- a: 1 # comment too close\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf)
+ self.check('---\n'
+ '- a: 1 # yamllint disable-line\n'
+ ' b:\n'
+ ' # yamllint disable-line\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf)
+ self.check('---\n'
+ '- a: 1 # yamllint disable-line rule:comments\n'
+ ' b:\n'
+ ' # yamllint disable-line rule:comments-indentation\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf)
+ self.check('---\n'
+ '# yamllint disable\n'
+ '- a: 1 # comment too close\n'
+ ' # yamllint enable rule:comments-indentation\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf,
+ problem=(6, 2, 'comments-indentation'))
+
+ def test_disable_file_directive(self):
+ conf = ('comments: {min-spaces-from-content: 2}\n'
+ 'comments-indentation: {}\n')
+ self.check('# yamllint disable-file\n'
+ '---\n'
+ '- a: 1 # comment too close\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf)
+ self.check('# yamllint disable-file\n'
+ '---\n'
+ '- a: 1 # comment too close\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf)
+ self.check('#yamllint disable-file\n'
+ '---\n'
+ '- a: 1 # comment too close\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf)
+ self.check('#yamllint disable-file \n'
+ '---\n'
+ '- a: 1 # comment too close\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf)
+ self.check('---\n'
+ '# yamllint disable-file\n'
+ '- a: 1 # comment too close\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf,
+ problem1=(3, 8, 'comments'),
+ problem2=(5, 2, 'comments-indentation'))
+ self.check('# yamllint disable-file: rules cannot be specified\n'
+ '---\n'
+ '- a: 1 # comment too close\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf,
+ problem1=(3, 8, 'comments'),
+ problem2=(5, 2, 'comments-indentation'))
+ self.check('AAAA yamllint disable-file\n'
+ '---\n'
+ '- a: 1 # comment too close\n'
+ ' b:\n'
+ ' # wrong indentation\n'
+ ' c: [x]\n',
+ conf,
+ problem1=(1, 1, 'document-start'),
+ problem2=(3, 8, 'comments'),
+ problem3=(5, 2, 'comments-indentation'))
+
+ def test_disable_file_directive_not_at_first_position(self):
+ self.check('# yamllint disable-file\n'
+ '---\n'
+ '- bad : colon and spaces \n',
+ self.conf)
+ self.check('---\n'
+ '# yamllint disable-file\n'
+ '- bad : colon and spaces \n',
+ self.conf,
+ problem1=(3, 7, 'colons'),
+ problem2=(3, 26, 'trailing-spaces'))
+
+ def test_disable_file_directive_with_syntax_error(self):
+ self.check('# This file is not valid YAML (it is a Jinja template)\n'
+ '{% if extra_info %}\n'
+ 'key1: value1\n'
+ '{% endif %}\n'
+ 'key2: value2\n',
+ self.conf,
+ problem=(2, 2, 'syntax'))
+ self.check('# yamllint disable-file\n'
+ '# This file is not valid YAML (it is a Jinja template)\n'
+ '{% if extra_info %}\n'
+ 'key1: value1\n'
+ '{% endif %}\n'
+ 'key2: value2\n',
+ self.conf)
+
+ def test_disable_file_directive_with_dos_lines(self):
+ self.check('# yamllint disable-file\r\n'
+ '---\r\n'
+ '- bad : colon and spaces \r\n',
+ self.conf)
+ self.check('# yamllint disable-file\r\n'
+ '# This file is not valid YAML (it is a Jinja template)\r\n'
+ '{% if extra_info %}\r\n'
+ 'key1: value1\r\n'
+ '{% endif %}\r\n'
+ 'key2: value2\r\n',
+ self.conf)
diff --git a/tests/yaml-1.2-spec-examples/example-10.1 b/tests/yaml-1.2-spec-examples/example-10.1
new file mode 100644
index 0000000..19c9782
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-10.1
@@ -0,0 +1,6 @@
+Block style: !!map
+ Clark : Evans
+ Ingy : döt Net
+ Oren : Ben-Kiki
+
+Flow style: !!map { Clark: Evans, Ingy: döt Net, Oren: Ben-Kiki }
diff --git a/tests/yaml-1.2-spec-examples/example-10.2 b/tests/yaml-1.2-spec-examples/example-10.2
new file mode 100644
index 0000000..63899c3
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-10.2
@@ -0,0 +1,6 @@
+Block style: !!seq
+- Clark Evans
+- Ingy döt Net
+- Oren Ben-Kiki
+
+Flow style: !!seq [ Clark Evans, Ingy döt Net, Oren Ben-Kiki ]
diff --git a/tests/yaml-1.2-spec-examples/example-10.3 b/tests/yaml-1.2-spec-examples/example-10.3
new file mode 100644
index 0000000..50e83bc
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-10.3
@@ -0,0 +1,4 @@
+Block style: !!str |-
+ String: just a theory.
+
+Flow style: !!str "String: just a theory."
diff --git a/tests/yaml-1.2-spec-examples/example-10.4 b/tests/yaml-1.2-spec-examples/example-10.4
new file mode 100644
index 0000000..7529872
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-10.4
@@ -0,0 +1,2 @@
+!!null null: value for null key
+key with null value: !!null null
diff --git a/tests/yaml-1.2-spec-examples/example-10.5 b/tests/yaml-1.2-spec-examples/example-10.5
new file mode 100644
index 0000000..2c11cad
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-10.5
@@ -0,0 +1,2 @@
+YAML is a superset of JSON: !!bool true
+Pluto is a planet: !!bool false
diff --git a/tests/yaml-1.2-spec-examples/example-10.6 b/tests/yaml-1.2-spec-examples/example-10.6
new file mode 100644
index 0000000..79fceea
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-10.6
@@ -0,0 +1,3 @@
+negative: !!int -12
+zero: !!int 0
+positive: !!int 34
diff --git a/tests/yaml-1.2-spec-examples/example-10.7 b/tests/yaml-1.2-spec-examples/example-10.7
new file mode 100644
index 0000000..f924530
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-10.7
@@ -0,0 +1,5 @@
+negative: !!float -1
+zero: !!float 0
+positive: !!float 2.3e4
+infinity: !!float .inf
+not a number: !!float .nan
diff --git a/tests/yaml-1.2-spec-examples/example-10.8 b/tests/yaml-1.2-spec-examples/example-10.8
new file mode 100644
index 0000000..552ff82
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-10.8
@@ -0,0 +1,5 @@
+A null: null
+Booleans: [ true, false ]
+Integers: [ 0, -0, 3, -19 ]
+Floats: [ 0., -0.0, 12e03, -2E+05 ]
+Invalid: [ True, Null, 0o7, 0x3A, +12.3 ]
diff --git a/tests/yaml-1.2-spec-examples/example-10.9 b/tests/yaml-1.2-spec-examples/example-10.9
new file mode 100644
index 0000000..28b8111
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-10.9
@@ -0,0 +1,7 @@
+A null: null
+Also a null: # Empty
+Not a null: ""
+Booleans: [ true, True, false, FALSE ]
+Integers: [ 0, 0o7, 0x3A, -19 ]
+Floats: [ 0., -0.0, .5, +12e03, -2E+05 ]
+Also floats: [ .inf, -.Inf, +.INF, .NAN ]
diff --git a/tests/yaml-1.2-spec-examples/example-2.1 b/tests/yaml-1.2-spec-examples/example-2.1
new file mode 100644
index 0000000..d12e671
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.1
@@ -0,0 +1,3 @@
+- Mark McGwire
+- Sammy Sosa
+- Ken Griffey
diff --git a/tests/yaml-1.2-spec-examples/example-2.10 b/tests/yaml-1.2-spec-examples/example-2.10
new file mode 100644
index 0000000..61808f6
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.10
@@ -0,0 +1,8 @@
+---
+hr:
+ - Mark McGwire
+ # Following node labeled SS
+ - &SS Sammy Sosa
+rbi:
+ - *SS # Subsequent occurrence
+ - Ken Griffey
diff --git a/tests/yaml-1.2-spec-examples/example-2.11 b/tests/yaml-1.2-spec-examples/example-2.11
new file mode 100644
index 0000000..9123ce2
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.11
@@ -0,0 +1,9 @@
+? - Detroit Tigers
+ - Chicago cubs
+:
+ - 2001-07-23
+
+? [ New York Yankees,
+ Atlanta Braves ]
+: [ 2001-07-02, 2001-08-12,
+ 2001-08-14 ]
diff --git a/tests/yaml-1.2-spec-examples/example-2.12 b/tests/yaml-1.2-spec-examples/example-2.12
new file mode 100644
index 0000000..8125296
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.12
@@ -0,0 +1,9 @@
+---
+# Products purchased
+- item : Super Hoop
+ quantity: 1
+- item : Basketball
+ quantity: 4
+- item : Big Shoes
+ quantity: 1
+
diff --git a/tests/yaml-1.2-spec-examples/example-2.13 b/tests/yaml-1.2-spec-examples/example-2.13
new file mode 100644
index 0000000..13fb656
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.13
@@ -0,0 +1,4 @@
+# ASCII Art
+--- |
+ \//||\/||
+ // || ||__
diff --git a/tests/yaml-1.2-spec-examples/example-2.14 b/tests/yaml-1.2-spec-examples/example-2.14
new file mode 100644
index 0000000..fb4ed4a
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.14
@@ -0,0 +1,4 @@
+--- >
+ Mark McGwire's
+ year was crippled
+ by a knee injury.
diff --git a/tests/yaml-1.2-spec-examples/example-2.15 b/tests/yaml-1.2-spec-examples/example-2.15
new file mode 100644
index 0000000..80b89a6
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.15
@@ -0,0 +1,8 @@
+>
+ Sammy Sosa completed another
+ fine season with great stats.
+
+ 63 Home Runs
+ 0.288 Batting Average
+
+ What a year!
diff --git a/tests/yaml-1.2-spec-examples/example-2.16 b/tests/yaml-1.2-spec-examples/example-2.16
new file mode 100644
index 0000000..223ec81
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.16
@@ -0,0 +1,8 @@
+name: Mark McGwire
+accomplishment: >
+ Mark set a major league
+ home run record in 1998.
+stats: |
+ 65 Home Runs
+ 0.278 Batting Average
+
diff --git a/tests/yaml-1.2-spec-examples/example-2.17 b/tests/yaml-1.2-spec-examples/example-2.17
new file mode 100644
index 0000000..c5c2a18
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.17
@@ -0,0 +1,7 @@
+unicode: "Sosa did fine.\u263A"
+control: "\b1998\t1999\t2000\n"
+hex esc: "\x0d\x0a is \r\n"
+
+single: '"Howdy!" he cried.'
+quoted: ' # Not a ''comment''.'
+tie-fighter: '|\-*-/|'
diff --git a/tests/yaml-1.2-spec-examples/example-2.18 b/tests/yaml-1.2-spec-examples/example-2.18
new file mode 100644
index 0000000..0f49d9c
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.18
@@ -0,0 +1,7 @@
+plain:
+ This unquoted scalar
+ spans many lines.
+
+quoted: "So does this
+ quoted scalar.\n"
+
diff --git a/tests/yaml-1.2-spec-examples/example-2.19 b/tests/yaml-1.2-spec-examples/example-2.19
new file mode 100644
index 0000000..843b149
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.19
@@ -0,0 +1,5 @@
+canonical: 12345
+decimal: +12345
+octal: 0o14
+hexadecimal: 0xC
+
diff --git a/tests/yaml-1.2-spec-examples/example-2.2 b/tests/yaml-1.2-spec-examples/example-2.2
new file mode 100644
index 0000000..7b7ec94
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.2
@@ -0,0 +1,3 @@
+hr: 65 # Home runs
+avg: 0.278 # Batting average
+rbi: 147 # Runs Batted In
diff --git a/tests/yaml-1.2-spec-examples/example-2.20 b/tests/yaml-1.2-spec-examples/example-2.20
new file mode 100644
index 0000000..499cbb1
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.20
@@ -0,0 +1,5 @@
+canonical: 1.23015e+3
+exponential: 12.3015e+02
+fixed: 1230.15
+negative infinity: -.inf
+not a number: .NaN
diff --git a/tests/yaml-1.2-spec-examples/example-2.21 b/tests/yaml-1.2-spec-examples/example-2.21
new file mode 100644
index 0000000..510165d
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.21
@@ -0,0 +1,3 @@
+null:
+booleans: [ true, false ]
+string: '012345'
diff --git a/tests/yaml-1.2-spec-examples/example-2.22 b/tests/yaml-1.2-spec-examples/example-2.22
new file mode 100644
index 0000000..aaac185
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.22
@@ -0,0 +1,4 @@
+canonical: 2001-12-15T02:59:43.1Z
+iso8601: 2001-12-14t21:59:43.10-05:00
+spaced: 2001-12-14 21:59:43.10 -5
+date: 2002-12-14
diff --git a/tests/yaml-1.2-spec-examples/example-2.23 b/tests/yaml-1.2-spec-examples/example-2.23
new file mode 100644
index 0000000..de1a732
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.23
@@ -0,0 +1,14 @@
+---
+not-date: !!str 2002-04-28
+
+picture: !!binary |
+ R0lGODlhDAAMAIQAAP//9/X
+ 17unp5WZmZgAAAOfn515eXv
+ Pz7Y6OjuDg4J+fn5OTk6enp
+ 56enmleECcgggoBADs=
+
+application specific tag: !something |
+ The semantics of the tag
+ above may be different for
+ different documents.
+
diff --git a/tests/yaml-1.2-spec-examples/example-2.24 b/tests/yaml-1.2-spec-examples/example-2.24
new file mode 100644
index 0000000..1180757
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.24
@@ -0,0 +1,14 @@
+%TAG ! tag:clarkevans.com,2002:
+--- !shape
+ # Use the ! handle for presenting
+ # tag:clarkevans.com,2002:circle
+- !circle
+ center: &ORIGIN {x: 73, y: 129}
+ radius: 7
+- !line
+ start: *ORIGIN
+ finish: { x: 89, y: 102 }
+- !label
+ start: *ORIGIN
+ color: 0xFFEEBB
+ text: Pretty vector drawing.
diff --git a/tests/yaml-1.2-spec-examples/example-2.25 b/tests/yaml-1.2-spec-examples/example-2.25
new file mode 100644
index 0000000..cf4943a
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.25
@@ -0,0 +1,7 @@
+# Sets are represented as a
+# Mapping where each key is
+# associated with a null value
+--- !!set
+? Mark McGwire
+? Sammy Sosa
+? Ken Griff
diff --git a/tests/yaml-1.2-spec-examples/example-2.26 b/tests/yaml-1.2-spec-examples/example-2.26
new file mode 100644
index 0000000..a28a7ac
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.26
@@ -0,0 +1,7 @@
+# Ordered maps are represented as
+# A sequence of mappings, with
+# each mapping having one key
+--- !!omap
+- Mark McGwire: 65
+- Sammy Sosa: 63
+- Ken Griffy: 58
diff --git a/tests/yaml-1.2-spec-examples/example-2.27 b/tests/yaml-1.2-spec-examples/example-2.27
new file mode 100644
index 0000000..4625739
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.27
@@ -0,0 +1,29 @@
+--- !<tag:clarkevans.com,2002:invoice>
+invoice: 34843
+date : 2001-01-23
+bill-to: &id001
+ given : Chris
+ family : Dumars
+ address:
+ lines: |
+ 458 Walkman Dr.
+ Suite #292
+ city : Royal Oak
+ state : MI
+ postal : 48046
+ship-to: *id001
+product:
+ - sku : BL394D
+ quantity : 4
+ description : Basketball
+ price : 450.00
+ - sku : BL4438H
+ quantity : 1
+ description : Super Hoop
+ price : 2392.00
+tax : 251.42
+total: 4443.52
+comments:
+ Late afternoon is best.
+ Backup contact is Nancy
+ Billsmer @ 338-4338.
diff --git a/tests/yaml-1.2-spec-examples/example-2.28 b/tests/yaml-1.2-spec-examples/example-2.28
new file mode 100644
index 0000000..eb5fb8a
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.28
@@ -0,0 +1,29 @@
+---
+Time: 2001-11-23 15:01:42 -5
+User: ed
+Warning:
+ This is an error message
+ for the log file
+---
+Time: 2001-11-23 15:02:31 -5
+User: ed
+Warning:
+ A slightly different error
+ message.
+---
+Date: 2001-11-23 15:03:17 -5
+User: ed
+Fatal:
+ Unknown variable "bar"
+Stack:
+ - file: TopClass.py
+ line: 23
+ code: |
+ x = MoreObject("345\n")
+ - file: MoreClass.py
+ line: 58
+ code: |-
+ foo = bar
+
+
+
diff --git a/tests/yaml-1.2-spec-examples/example-2.3 b/tests/yaml-1.2-spec-examples/example-2.3
new file mode 100644
index 0000000..656d628
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.3
@@ -0,0 +1,8 @@
+american:
+ - Boston Red Sox
+ - Detroit Tigers
+ - New York Yankees
+national:
+ - New York Mets
+ - Chicago Cubs
+ - Atlanta Braves
diff --git a/tests/yaml-1.2-spec-examples/example-2.4 b/tests/yaml-1.2-spec-examples/example-2.4
new file mode 100644
index 0000000..430f6b3
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.4
@@ -0,0 +1,8 @@
+-
+ name: Mark McGwire
+ hr: 65
+ avg: 0.278
+-
+ name: Sammy Sosa
+ hr: 63
+ avg: 0.288
diff --git a/tests/yaml-1.2-spec-examples/example-2.5 b/tests/yaml-1.2-spec-examples/example-2.5
new file mode 100644
index 0000000..9aafb4e
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.5
@@ -0,0 +1,5 @@
+- [name , hr, avg ]
+- [Mark McGwire, 65, 0.278]
+- [Sammy Sosa , 63, 0.288]
+
+
diff --git a/tests/yaml-1.2-spec-examples/example-2.6 b/tests/yaml-1.2-spec-examples/example-2.6
new file mode 100644
index 0000000..7a957b2
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.6
@@ -0,0 +1,5 @@
+Mark McGwire: {hr: 65, avg: 0.278}
+Sammy Sosa: {
+ hr: 63,
+ avg: 0.288
+ }
diff --git a/tests/yaml-1.2-spec-examples/example-2.7 b/tests/yaml-1.2-spec-examples/example-2.7
new file mode 100644
index 0000000..bc711d5
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.7
@@ -0,0 +1,10 @@
+# Ranking of 1998 home runs
+---
+- Mark McGwire
+- Sammy Sosa
+- Ken Griffey
+
+# Team ranking
+---
+- Chicago Cubs
+- St Louis Cardinals
diff --git a/tests/yaml-1.2-spec-examples/example-2.8 b/tests/yaml-1.2-spec-examples/example-2.8
new file mode 100644
index 0000000..05e102d
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.8
@@ -0,0 +1,10 @@
+---
+time: 20:03:20
+player: Sammy Sosa
+action: strike (miss)
+...
+---
+time: 20:03:47
+player: Sammy Sosa
+action: grand slam
+...
diff --git a/tests/yaml-1.2-spec-examples/example-2.9 b/tests/yaml-1.2-spec-examples/example-2.9
new file mode 100644
index 0000000..e264180
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-2.9
@@ -0,0 +1,8 @@
+---
+hr: # 1998 hr ranking
+ - Mark McGwire
+ - Sammy Sosa
+rbi:
+ # 1998 rbi ranking
+ - Sammy Sosa
+ - Ken Griffey
diff --git a/tests/yaml-1.2-spec-examples/example-5.1 b/tests/yaml-1.2-spec-examples/example-5.1
new file mode 100644
index 0000000..62524c0
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.1
@@ -0,0 +1 @@
+# Comment only.
diff --git a/tests/yaml-1.2-spec-examples/example-5.10 b/tests/yaml-1.2-spec-examples/example-5.10
new file mode 100644
index 0000000..a4caf91
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.10
@@ -0,0 +1,2 @@
+commercial-at: @text
+grave-accent: `text
diff --git a/tests/yaml-1.2-spec-examples/example-5.11 b/tests/yaml-1.2-spec-examples/example-5.11
new file mode 100644
index 0000000..f980428
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.11
@@ -0,0 +1,3 @@
+|
+ Line break (no glyph)
+ Line break (glyphed)
diff --git a/tests/yaml-1.2-spec-examples/example-5.12 b/tests/yaml-1.2-spec-examples/example-5.12
new file mode 100644
index 0000000..af9a321
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.12
@@ -0,0 +1,6 @@
+# Tabs and spaces
+quoted: "Quoted "
+block: |
+ void main() {
+ printf("Hello, world!\n");
+ }
diff --git a/tests/yaml-1.2-spec-examples/example-5.13 b/tests/yaml-1.2-spec-examples/example-5.13
new file mode 100644
index 0000000..a8f1b48
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.13
@@ -0,0 +1,5 @@
+"Fun with \\
+\" \a \b \e \f \
+\n \r \t \v \0 \
+\  \_ \N \L \P \
+\x41 \u0041 \U00000041"
diff --git a/tests/yaml-1.2-spec-examples/example-5.14 b/tests/yaml-1.2-spec-examples/example-5.14
new file mode 100644
index 0000000..7bf12b6
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.14
@@ -0,0 +1,3 @@
+Bad escapes:
+ "\c
+ \xq-"
diff --git a/tests/yaml-1.2-spec-examples/example-5.2 b/tests/yaml-1.2-spec-examples/example-5.2
new file mode 100644
index 0000000..9f1ca25
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.2
@@ -0,0 +1,3 @@
+- Invalid use of BOM
+
+- Inside a document.
diff --git a/tests/yaml-1.2-spec-examples/example-5.3 b/tests/yaml-1.2-spec-examples/example-5.3
new file mode 100644
index 0000000..608ea19
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.3
@@ -0,0 +1,7 @@
+sequence:
+- one
+- two
+mapping:
+ ? sky
+ : blue
+ sea : green
diff --git a/tests/yaml-1.2-spec-examples/example-5.4 b/tests/yaml-1.2-spec-examples/example-5.4
new file mode 100644
index 0000000..df33847
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.4
@@ -0,0 +1,2 @@
+sequence: [ one, two, ]
+mapping: { sky: blue, sea: green }
diff --git a/tests/yaml-1.2-spec-examples/example-5.5 b/tests/yaml-1.2-spec-examples/example-5.5
new file mode 100644
index 0000000..62524c0
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.5
@@ -0,0 +1 @@
+# Comment only.
diff --git a/tests/yaml-1.2-spec-examples/example-5.6 b/tests/yaml-1.2-spec-examples/example-5.6
new file mode 100644
index 0000000..7a1f9b3
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.6
@@ -0,0 +1,2 @@
+anchored: !local &anchor value
+alias: *anchor
diff --git a/tests/yaml-1.2-spec-examples/example-5.7 b/tests/yaml-1.2-spec-examples/example-5.7
new file mode 100644
index 0000000..934726c
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.7
@@ -0,0 +1,6 @@
+literal: |
+ some
+ text
+folded: >
+ some
+ text
diff --git a/tests/yaml-1.2-spec-examples/example-5.8 b/tests/yaml-1.2-spec-examples/example-5.8
new file mode 100644
index 0000000..04ebf69
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.8
@@ -0,0 +1,2 @@
+single: 'text'
+double: "text"
diff --git a/tests/yaml-1.2-spec-examples/example-5.9 b/tests/yaml-1.2-spec-examples/example-5.9
new file mode 100644
index 0000000..62204de
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-5.9
@@ -0,0 +1,2 @@
+%YAML 1.2
+--- text
diff --git a/tests/yaml-1.2-spec-examples/example-6.1 b/tests/yaml-1.2-spec-examples/example-6.1
new file mode 100644
index 0000000..b5496c1
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.1
@@ -0,0 +1,12 @@
+ # Leading comment line spaces are
+ # neither content nor indentation.
+
+Not indented:
+ By one space: |
+ By four
+ spaces
+ Flow style: [ # Leading spaces
+ By two, # in flow style
+ Also by two, # are neither
+ Still by two # content nor
+ ] # indentation.
diff --git a/tests/yaml-1.2-spec-examples/example-6.10 b/tests/yaml-1.2-spec-examples/example-6.10
new file mode 100644
index 0000000..ff741e5
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.10
@@ -0,0 +1,3 @@
+ # Comment
+
+
diff --git a/tests/yaml-1.2-spec-examples/example-6.11 b/tests/yaml-1.2-spec-examples/example-6.11
new file mode 100644
index 0000000..86308dd
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.11
@@ -0,0 +1,4 @@
+key: # Comment
+ # lines
+ value
+
diff --git a/tests/yaml-1.2-spec-examples/example-6.12 b/tests/yaml-1.2-spec-examples/example-6.12
new file mode 100644
index 0000000..e1e1113
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.12
@@ -0,0 +1,6 @@
+{ first: Sammy, last: Sosa }:
+# Statistics:
+ hr: # Home runs
+ 65
+ avg: # Average
+ 0.278
diff --git a/tests/yaml-1.2-spec-examples/example-6.13 b/tests/yaml-1.2-spec-examples/example-6.13
new file mode 100644
index 0000000..2113eb6
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.13
@@ -0,0 +1,3 @@
+%FOO bar baz # Should be ignored
+ # with a warning.
+--- "foo"
diff --git a/tests/yaml-1.2-spec-examples/example-6.14 b/tests/yaml-1.2-spec-examples/example-6.14
new file mode 100644
index 0000000..ef326d5
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.14
@@ -0,0 +1,4 @@
+%YAML 1.3 # Attempt parsing
+ # with a warning
+---
+"foo"
diff --git a/tests/yaml-1.2-spec-examples/example-6.15 b/tests/yaml-1.2-spec-examples/example-6.15
new file mode 100644
index 0000000..acff4e8
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.15
@@ -0,0 +1,3 @@
+%YAML 1.2
+%YAML 1.1
+foo
diff --git a/tests/yaml-1.2-spec-examples/example-6.16 b/tests/yaml-1.2-spec-examples/example-6.16
new file mode 100644
index 0000000..50f5ab9
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.16
@@ -0,0 +1,3 @@
+%TAG !yaml! tag:yaml.org,2002:
+---
+!yaml!str "foo"
diff --git a/tests/yaml-1.2-spec-examples/example-6.17 b/tests/yaml-1.2-spec-examples/example-6.17
new file mode 100644
index 0000000..7276eae
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.17
@@ -0,0 +1,3 @@
+%TAG ! !foo
+%TAG ! !foo
+bar
diff --git a/tests/yaml-1.2-spec-examples/example-6.18 b/tests/yaml-1.2-spec-examples/example-6.18
new file mode 100644
index 0000000..d79f04e
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.18
@@ -0,0 +1,7 @@
+# Private
+!foo "bar"
+...
+# Global
+%TAG ! tag:example.com,2000:app/
+---
+!foo "bar"
diff --git a/tests/yaml-1.2-spec-examples/example-6.19 b/tests/yaml-1.2-spec-examples/example-6.19
new file mode 100644
index 0000000..7b9d9b1
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.19
@@ -0,0 +1,3 @@
+%TAG !! tag:example.com,2000:app/
+---
+!!int 1 - 3 # Interval, not integer
diff --git a/tests/yaml-1.2-spec-examples/example-6.2 b/tests/yaml-1.2-spec-examples/example-6.2
new file mode 100644
index 0000000..ac0d970
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.2
@@ -0,0 +1,4 @@
+? a
+: - b
+ - - c
+ - d
diff --git a/tests/yaml-1.2-spec-examples/example-6.20 b/tests/yaml-1.2-spec-examples/example-6.20
new file mode 100644
index 0000000..690f138
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.20
@@ -0,0 +1,3 @@
+%TAG !e! tag:example.com,2000:app/
+---
+!e!foo "bar"
diff --git a/tests/yaml-1.2-spec-examples/example-6.21 b/tests/yaml-1.2-spec-examples/example-6.21
new file mode 100644
index 0000000..57315a5
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.21
@@ -0,0 +1,7 @@
+%TAG !m! !my-
+--- # Bulb here
+!m!light fluorescent
+...
+%TAG !m! !my-
+--- # Color here
+!m!light green
diff --git a/tests/yaml-1.2-spec-examples/example-6.22 b/tests/yaml-1.2-spec-examples/example-6.22
new file mode 100644
index 0000000..eedfe04
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.22
@@ -0,0 +1,3 @@
+%TAG !e! tag:example.com,2000:app/
+---
+- !e!foo "bar"
diff --git a/tests/yaml-1.2-spec-examples/example-6.23 b/tests/yaml-1.2-spec-examples/example-6.23
new file mode 100644
index 0000000..66d75f3
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.23
@@ -0,0 +1,3 @@
+!!str &a1 "foo":
+ !!str bar
+&a2 baz : *a1
diff --git a/tests/yaml-1.2-spec-examples/example-6.24 b/tests/yaml-1.2-spec-examples/example-6.24
new file mode 100644
index 0000000..8e51f52
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.24
@@ -0,0 +1,2 @@
+!<tag:yaml.org,2002:str> foo :
+ !<!bar> baz
diff --git a/tests/yaml-1.2-spec-examples/example-6.25 b/tests/yaml-1.2-spec-examples/example-6.25
new file mode 100644
index 0000000..f7d1b01
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.25
@@ -0,0 +1,2 @@
+- !<!> foo
+- !<$:?> bar
diff --git a/tests/yaml-1.2-spec-examples/example-6.26 b/tests/yaml-1.2-spec-examples/example-6.26
new file mode 100644
index 0000000..70365f4
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.26
@@ -0,0 +1,5 @@
+%TAG !e! tag:example.com,2000:app/
+---
+- !local foo
+- !!str bar
+- !e!tag%21 baz
diff --git a/tests/yaml-1.2-spec-examples/example-6.27 b/tests/yaml-1.2-spec-examples/example-6.27
new file mode 100644
index 0000000..d7fff4e
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.27
@@ -0,0 +1,4 @@
+%TAG !e! tag:example,2000:app/
+---
+- !e! foo
+- !h!bar baz
diff --git a/tests/yaml-1.2-spec-examples/example-6.28 b/tests/yaml-1.2-spec-examples/example-6.28
new file mode 100644
index 0000000..98aa565
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.28
@@ -0,0 +1,4 @@
+# Assuming conventional resolution:
+- "12"
+- 12
+- ! 12
diff --git a/tests/yaml-1.2-spec-examples/example-6.29 b/tests/yaml-1.2-spec-examples/example-6.29
new file mode 100644
index 0000000..600d179
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.29
@@ -0,0 +1,2 @@
+First occurrence: &anchor Value
+Second occurrence: *anchor
diff --git a/tests/yaml-1.2-spec-examples/example-6.3 b/tests/yaml-1.2-spec-examples/example-6.3
new file mode 100644
index 0000000..5f48cf4
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.3
@@ -0,0 +1,3 @@
+- foo: bar
+- - baz
+ - baz
diff --git a/tests/yaml-1.2-spec-examples/example-6.4 b/tests/yaml-1.2-spec-examples/example-6.4
new file mode 100644
index 0000000..2f62d08
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.4
@@ -0,0 +1,7 @@
+plain: text
+ lines
+quoted: "text
+ lines"
+block: |
+ text
+ lines
diff --git a/tests/yaml-1.2-spec-examples/example-6.5 b/tests/yaml-1.2-spec-examples/example-6.5
new file mode 100644
index 0000000..8ea3e52
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.5
@@ -0,0 +1,7 @@
+Folding:
+ "Empty line
+
+ as a line feed"
+Chomping: |
+ Clipped empty lines
+
diff --git a/tests/yaml-1.2-spec-examples/example-6.6 b/tests/yaml-1.2-spec-examples/example-6.6
new file mode 100644
index 0000000..1c5090d
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.6
@@ -0,0 +1,7 @@
+>-
+ trimmed
+
+
+
+ as
+ space
diff --git a/tests/yaml-1.2-spec-examples/example-6.7 b/tests/yaml-1.2-spec-examples/example-6.7
new file mode 100644
index 0000000..0896cc6
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.7
@@ -0,0 +1,6 @@
+>
+ foo
+
+ bar
+
+ baz
diff --git a/tests/yaml-1.2-spec-examples/example-6.8 b/tests/yaml-1.2-spec-examples/example-6.8
new file mode 100644
index 0000000..d6af812
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.8
@@ -0,0 +1,7 @@
+"
+ foo
+
+ bar
+
+ baz
+"
diff --git a/tests/yaml-1.2-spec-examples/example-6.9 b/tests/yaml-1.2-spec-examples/example-6.9
new file mode 100644
index 0000000..9a94fc1
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-6.9
@@ -0,0 +1,2 @@
+key: # Comment
+ valueeof
diff --git a/tests/yaml-1.2-spec-examples/example-7.1 b/tests/yaml-1.2-spec-examples/example-7.1
new file mode 100644
index 0000000..3887676
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.1
@@ -0,0 +1,4 @@
+First occurrence: &anchor Foo
+Second occurrence: *anchor
+Override anchor: &anchor Bar
+Reuse anchor: *anchor
diff --git a/tests/yaml-1.2-spec-examples/example-7.10 b/tests/yaml-1.2-spec-examples/example-7.10
new file mode 100644
index 0000000..7ed369f
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.10
@@ -0,0 +1,12 @@
+# Outside flow collection:
+- ::vector
+- ": - ()"
+- Up, up, and away!
+- -123
+- http://example.com/foo#bar
+# Inside flow collection:
+- [ ::vector,
+ ": - ()",
+ "Up, up and away!",
+ -123,
+ http://example.com/foo#bar ]
diff --git a/tests/yaml-1.2-spec-examples/example-7.11 b/tests/yaml-1.2-spec-examples/example-7.11
new file mode 100644
index 0000000..fd57f65
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.11
@@ -0,0 +1,3 @@
+implicit block key : [
+ implicit flow key : value,
+ ]
diff --git a/tests/yaml-1.2-spec-examples/example-7.12 b/tests/yaml-1.2-spec-examples/example-7.12
new file mode 100644
index 0000000..0499250
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.12
@@ -0,0 +1,4 @@
+1st non-empty
+
+ 2nd non-empty
+ 3rd non-empty
diff --git a/tests/yaml-1.2-spec-examples/example-7.13 b/tests/yaml-1.2-spec-examples/example-7.13
new file mode 100644
index 0000000..cd77480
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.13
@@ -0,0 +1,2 @@
+- [ one, two, ]
+- [three ,four]
diff --git a/tests/yaml-1.2-spec-examples/example-7.14 b/tests/yaml-1.2-spec-examples/example-7.14
new file mode 100644
index 0000000..6327116
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.14
@@ -0,0 +1,8 @@
+[
+"double
+ quoted", 'single
+ quoted',
+plain
+ text, [ nested ],
+single: pair,
+]
diff --git a/tests/yaml-1.2-spec-examples/example-7.15 b/tests/yaml-1.2-spec-examples/example-7.15
new file mode 100644
index 0000000..0718643
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.15
@@ -0,0 +1,2 @@
+- { one : two , three: four , }
+- {five: six,seven : eight}
diff --git a/tests/yaml-1.2-spec-examples/example-7.16 b/tests/yaml-1.2-spec-examples/example-7.16
new file mode 100644
index 0000000..cb84a99
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.16
@@ -0,0 +1,5 @@
+{
+? explicit: entry,
+implicit: entry,
+?
+}
diff --git a/tests/yaml-1.2-spec-examples/example-7.17 b/tests/yaml-1.2-spec-examples/example-7.17
new file mode 100644
index 0000000..3cc1296
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.17
@@ -0,0 +1,6 @@
+{
+unquoted : "separate",
+http://foo.com,
+omitted value:,
+: omitted key,
+}
diff --git a/tests/yaml-1.2-spec-examples/example-7.18 b/tests/yaml-1.2-spec-examples/example-7.18
new file mode 100644
index 0000000..7fc069c
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.18
@@ -0,0 +1,5 @@
+{
+"adjacent":value,
+"readable": value,
+"empty":
+}
diff --git a/tests/yaml-1.2-spec-examples/example-7.19 b/tests/yaml-1.2-spec-examples/example-7.19
new file mode 100644
index 0000000..77f3eb3
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.19
@@ -0,0 +1,3 @@
+[
+foo: bar
+]
diff --git a/tests/yaml-1.2-spec-examples/example-7.2 b/tests/yaml-1.2-spec-examples/example-7.2
new file mode 100644
index 0000000..aa86103
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.2
@@ -0,0 +1,4 @@
+{
+ foo : !!str,
+ !!str : bar,
+}
diff --git a/tests/yaml-1.2-spec-examples/example-7.20 b/tests/yaml-1.2-spec-examples/example-7.20
new file mode 100644
index 0000000..19dc4f5
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.20
@@ -0,0 +1,4 @@
+[
+? foo
+ bar : baz
+]
diff --git a/tests/yaml-1.2-spec-examples/example-7.21 b/tests/yaml-1.2-spec-examples/example-7.21
new file mode 100644
index 0000000..fdff3b5
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.21
@@ -0,0 +1,3 @@
+- [ YAML : separate ]
+- [ : empty key entry ]
+- [ {JSON: like}:adjacent ]
diff --git a/tests/yaml-1.2-spec-examples/example-7.22 b/tests/yaml-1.2-spec-examples/example-7.22
new file mode 100644
index 0000000..85c6ccb
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.22
@@ -0,0 +1,3 @@
+[ foo
+ bar: invalid,
+ "foo...>1K characters...bar": invalid ]
diff --git a/tests/yaml-1.2-spec-examples/example-7.23 b/tests/yaml-1.2-spec-examples/example-7.23
new file mode 100644
index 0000000..f709dc8
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.23
@@ -0,0 +1,5 @@
+- [ a, b ]
+- { a: b }
+- "a"
+- 'b'
+- c
diff --git a/tests/yaml-1.2-spec-examples/example-7.24 b/tests/yaml-1.2-spec-examples/example-7.24
new file mode 100644
index 0000000..db4007f
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.24
@@ -0,0 +1,5 @@
+- !!str "a"
+- 'b'
+- &anchor "c"
+- *anchor
+- !!str
diff --git a/tests/yaml-1.2-spec-examples/example-7.3 b/tests/yaml-1.2-spec-examples/example-7.3
new file mode 100644
index 0000000..f46900d
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.3
@@ -0,0 +1,4 @@
+{
+ ? foo :,
+ : bar,
+}
diff --git a/tests/yaml-1.2-spec-examples/example-7.4 b/tests/yaml-1.2-spec-examples/example-7.4
new file mode 100644
index 0000000..1b7a550
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.4
@@ -0,0 +1,3 @@
+"implicit block key" : [
+ "implicit flow key" : value,
+ ]
diff --git a/tests/yaml-1.2-spec-examples/example-7.5 b/tests/yaml-1.2-spec-examples/example-7.5
new file mode 100644
index 0000000..eda4b49
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.5
@@ -0,0 +1,5 @@
+"folded
+to a space,
+
+to a line feed, or \
+ \ non-content"
diff --git a/tests/yaml-1.2-spec-examples/example-7.6 b/tests/yaml-1.2-spec-examples/example-7.6
new file mode 100644
index 0000000..3d8b76d
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.6
@@ -0,0 +1,4 @@
+" 1st non-empty
+
+ 2nd non-empty
+ 3rd non-empty "
diff --git a/tests/yaml-1.2-spec-examples/example-7.7 b/tests/yaml-1.2-spec-examples/example-7.7
new file mode 100644
index 0000000..b038078
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.7
@@ -0,0 +1 @@
+ 'here''s to "quotes"'
diff --git a/tests/yaml-1.2-spec-examples/example-7.8 b/tests/yaml-1.2-spec-examples/example-7.8
new file mode 100644
index 0000000..f1baf58
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.8
@@ -0,0 +1,3 @@
+'implicit block key' : [
+ 'implicit flow key' : value,
+ ]
diff --git a/tests/yaml-1.2-spec-examples/example-7.9 b/tests/yaml-1.2-spec-examples/example-7.9
new file mode 100644
index 0000000..6dd946e
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-7.9
@@ -0,0 +1,4 @@
+' 1st non-empty
+
+ 2nd non-empty
+ 3rd non-empty '
diff --git a/tests/yaml-1.2-spec-examples/example-8.1 b/tests/yaml-1.2-spec-examples/example-8.1
new file mode 100644
index 0000000..fea9c8b
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.1
@@ -0,0 +1,10 @@
+- | # Empty header
+ literal
+- >1 # Indentation indicator
+ folded
+- |+ # Chomping indicator
+ keep
+
+- >1- # Both indicators
+ strip
+
diff --git a/tests/yaml-1.2-spec-examples/example-8.10 b/tests/yaml-1.2-spec-examples/example-8.10
new file mode 100644
index 0000000..992dd76
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.10
@@ -0,0 +1,16 @@
+>
+
+ folded
+ line
+
+ next
+ line
+ * bullet
+
+ * list
+ * lines
+
+ last
+ line
+
+# Comment
diff --git a/tests/yaml-1.2-spec-examples/example-8.11 b/tests/yaml-1.2-spec-examples/example-8.11
new file mode 100644
index 0000000..992dd76
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.11
@@ -0,0 +1,16 @@
+>
+
+ folded
+ line
+
+ next
+ line
+ * bullet
+
+ * list
+ * lines
+
+ last
+ line
+
+# Comment
diff --git a/tests/yaml-1.2-spec-examples/example-8.12 b/tests/yaml-1.2-spec-examples/example-8.12
new file mode 100644
index 0000000..bd226b1
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.12
@@ -0,0 +1,16 @@
+>
+
+ folded
+ line
+
+ next
+ line
+ * bullet
+
+ * list
+ * line
+
+ last
+ line
+
+# Comment
diff --git a/tests/yaml-1.2-spec-examples/example-8.13 b/tests/yaml-1.2-spec-examples/example-8.13
new file mode 100644
index 0000000..624f219
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.13
@@ -0,0 +1,15 @@
+>
+ folded
+ line
+
+ next
+ line
+ * bullet
+
+ * list
+ * line
+
+ last
+ line
+
+# Comment
diff --git a/tests/yaml-1.2-spec-examples/example-8.14 b/tests/yaml-1.2-spec-examples/example-8.14
new file mode 100644
index 0000000..d2f2ccf
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.14
@@ -0,0 +1,3 @@
+block sequence:
+ - one
+ - two : three
diff --git a/tests/yaml-1.2-spec-examples/example-8.15 b/tests/yaml-1.2-spec-examples/example-8.15
new file mode 100644
index 0000000..35ac923
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.15
@@ -0,0 +1,6 @@
+- # Empty
+- |
+ block node
+- - one # Compact
+ - two # sequence
+- one: two # Compact mapping
diff --git a/tests/yaml-1.2-spec-examples/example-8.16 b/tests/yaml-1.2-spec-examples/example-8.16
new file mode 100644
index 0000000..2ef9084
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.16
@@ -0,0 +1,2 @@
+block mapping:
+ key: value
diff --git a/tests/yaml-1.2-spec-examples/example-8.17 b/tests/yaml-1.2-spec-examples/example-8.17
new file mode 100644
index 0000000..cb0cfd0
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.17
@@ -0,0 +1,5 @@
+? explicit key # Empty value
+? |
+ block key
+: - one # Explicit compact
+ - two # block value
diff --git a/tests/yaml-1.2-spec-examples/example-8.18 b/tests/yaml-1.2-spec-examples/example-8.18
new file mode 100644
index 0000000..c819512
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.18
@@ -0,0 +1,4 @@
+plain key: in-line value
+: # Both empty
+"quoted key":
+- entry
diff --git a/tests/yaml-1.2-spec-examples/example-8.19 b/tests/yaml-1.2-spec-examples/example-8.19
new file mode 100644
index 0000000..d675cfd
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.19
@@ -0,0 +1,3 @@
+- sun: yellow
+- ? earth: blue
+ : moon: white
diff --git a/tests/yaml-1.2-spec-examples/example-8.2 b/tests/yaml-1.2-spec-examples/example-8.2
new file mode 100644
index 0000000..39bee04
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.2
@@ -0,0 +1,11 @@
+- |
+ detected
+- >
+
+
+ # detected
+- |1
+ explicit
+- >
+
+ detected
diff --git a/tests/yaml-1.2-spec-examples/example-8.20 b/tests/yaml-1.2-spec-examples/example-8.20
new file mode 100644
index 0000000..a3f13ae
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.20
@@ -0,0 +1,6 @@
+-
+ "flow in block"
+- >
+ Block scalar
+- !!map # Block collection
+ foo : bar
diff --git a/tests/yaml-1.2-spec-examples/example-8.21 b/tests/yaml-1.2-spec-examples/example-8.21
new file mode 100644
index 0000000..f86be74
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.21
@@ -0,0 +1,6 @@
+literal: |2
+ value
+folded:
+ !foo
+ >1
+ value
diff --git a/tests/yaml-1.2-spec-examples/example-8.22 b/tests/yaml-1.2-spec-examples/example-8.22
new file mode 100644
index 0000000..5c59669
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.22
@@ -0,0 +1,6 @@
+sequence: !!seq
+- entry
+- !!seq
+ - nested
+mapping: !!map
+ foo: bar
diff --git a/tests/yaml-1.2-spec-examples/example-8.3 b/tests/yaml-1.2-spec-examples/example-8.3
new file mode 100644
index 0000000..46edf9f
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.3
@@ -0,0 +1,8 @@
+- |
+
+ text
+- >
+ text
+ text
+- |2
+ text
diff --git a/tests/yaml-1.2-spec-examples/example-8.4 b/tests/yaml-1.2-spec-examples/example-8.4
new file mode 100644
index 0000000..fa6190f
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.4
@@ -0,0 +1,6 @@
+strip: |-
+ text
+clip: |
+ text
+keep: |+
+ text
diff --git a/tests/yaml-1.2-spec-examples/example-8.5 b/tests/yaml-1.2-spec-examples/example-8.5
new file mode 100644
index 0000000..32fa08f
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.5
@@ -0,0 +1,19 @@
+ # Strip
+ # Comments:
+strip: |-
+ # text
+
+ # Clip
+ # comments:
+
+clip: |
+ # text
+
+ # Keep
+ # comments:
+
+keep: |+
+ # text
+
+ # Trail
+ # comments.
diff --git a/tests/yaml-1.2-spec-examples/example-8.6 b/tests/yaml-1.2-spec-examples/example-8.6
new file mode 100644
index 0000000..de0b64b
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.6
@@ -0,0 +1,6 @@
+strip: >-
+
+clip: >
+
+keep: |+
+
diff --git a/tests/yaml-1.2-spec-examples/example-8.7 b/tests/yaml-1.2-spec-examples/example-8.7
new file mode 100644
index 0000000..7fa415f
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.7
@@ -0,0 +1,4 @@
+|
+ literal
+ text
+
diff --git a/tests/yaml-1.2-spec-examples/example-8.8 b/tests/yaml-1.2-spec-examples/example-8.8
new file mode 100644
index 0000000..9d537cb
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.8
@@ -0,0 +1,9 @@
+|
+
+
+ literal
+
+
+ text
+
+ # Comment
diff --git a/tests/yaml-1.2-spec-examples/example-8.9 b/tests/yaml-1.2-spec-examples/example-8.9
new file mode 100644
index 0000000..c016ca9
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-8.9
@@ -0,0 +1,4 @@
+>
+ folded
+ text
+
diff --git a/tests/yaml-1.2-spec-examples/example-9.1 b/tests/yaml-1.2-spec-examples/example-9.1
new file mode 100644
index 0000000..59b6591
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-9.1
@@ -0,0 +1,3 @@
+# Comment
+# lines
+Document
diff --git a/tests/yaml-1.2-spec-examples/example-9.2 b/tests/yaml-1.2-spec-examples/example-9.2
new file mode 100644
index 0000000..886e574
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-9.2
@@ -0,0 +1,4 @@
+%YAML 1.2
+---
+Document
+... # Suffix
diff --git a/tests/yaml-1.2-spec-examples/example-9.3 b/tests/yaml-1.2-spec-examples/example-9.3
new file mode 100644
index 0000000..57423e9
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-9.3
@@ -0,0 +1,7 @@
+Bare
+document
+...
+# No document
+...
+|
+%!PS-Adobe-2.0 # Not the first line
diff --git a/tests/yaml-1.2-spec-examples/example-9.4 b/tests/yaml-1.2-spec-examples/example-9.4
new file mode 100644
index 0000000..bc363b1
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-9.4
@@ -0,0 +1,7 @@
+---
+{ matches
+% : 20 }
+...
+---
+# Empty
+...
diff --git a/tests/yaml-1.2-spec-examples/example-9.5 b/tests/yaml-1.2-spec-examples/example-9.5
new file mode 100644
index 0000000..de2463d
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-9.5
@@ -0,0 +1,8 @@
+%YAML 1.2
+--- |
+%!PS-Adobe-2.0
+...
+%YAML1.2
+---
+# Empty
+...
diff --git a/tests/yaml-1.2-spec-examples/example-9.6 b/tests/yaml-1.2-spec-examples/example-9.6
new file mode 100644
index 0000000..52bd345
--- /dev/null
+++ b/tests/yaml-1.2-spec-examples/example-9.6
@@ -0,0 +1,7 @@
+Document
+---
+# Empty
+...
+%YAML 1.2
+---
+matches %: 20
diff --git a/yamllint/__init__.py b/yamllint/__init__.py
new file mode 100644
index 0000000..907328e
--- /dev/null
+++ b/yamllint/__init__.py
@@ -0,0 +1,30 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""A linter for YAML files.
+
+yamllint does not only check for syntax validity, but for weirdnesses like key
+repetition and cosmetic problems such as lines length, trailing spaces,
+indentation, etc."""
+
+
+APP_NAME = 'yamllint'
+APP_VERSION = '1.33.0'
+APP_DESCRIPTION = __doc__
+
+__author__ = 'Adrien Vergé'
+__copyright__ = 'Copyright 2022, Adrien Vergé'
+__license__ = 'GPLv3'
+__version__ = APP_VERSION
diff --git a/yamllint/__main__.py b/yamllint/__main__.py
new file mode 100644
index 0000000..bc16534
--- /dev/null
+++ b/yamllint/__main__.py
@@ -0,0 +1,4 @@
+from yamllint.cli import run
+
+if __name__ == '__main__':
+ run()
diff --git a/yamllint/cli.py b/yamllint/cli.py
new file mode 100644
index 0000000..604e594
--- /dev/null
+++ b/yamllint/cli.py
@@ -0,0 +1,249 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import argparse
+import locale
+import os
+import platform
+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
+
+
+def find_files_recursively(items, conf):
+ for item in items:
+ if os.path.isdir(item):
+ for root, dirnames, filenames in os.walk(item):
+ for f in filenames:
+ filepath = os.path.join(root, f)
+ if conf.is_yaml_file(filepath):
+ yield filepath
+ else:
+ yield item
+
+
+def supports_color():
+ supported_platform = not (platform.system() == 'Windows' and not
+ ('ANSICON' in os.environ or
+ ('TERM' in os.environ and
+ os.environ['TERM'] == 'ANSI')))
+ return (supported_platform and
+ hasattr(sys.stdout, 'isatty') and sys.stdout.isatty())
+
+
+class Format:
+ @staticmethod
+ def parsable(problem, filename):
+ return (f'{filename}:{problem.line}:{problem.column}: '
+ f'[{problem.level}] {problem.message}')
+
+ @staticmethod
+ def standard(problem, filename):
+ line = f' {problem.line}:{problem.column}'
+ line += max(12 - len(line), 0) * ' '
+ line += problem.level
+ line += max(21 - len(line), 0) * ' '
+ line += problem.desc
+ if problem.rule:
+ line += f' ({problem.rule})'
+ return line
+
+ @staticmethod
+ def standard_color(problem, filename):
+ line = f' \033[2m{problem.line}:{problem.column}\033[0m'
+ line += max(20 - len(line), 0) * ' '
+ if problem.level == 'warning':
+ line += f'\033[33m{problem.level}\033[0m'
+ else:
+ line += f'\033[31m{problem.level}\033[0m'
+ line += max(38 - len(line), 0) * ' '
+ line += problem.desc
+ if problem.rule:
+ line += f' \033[2m({problem.rule})\033[0m'
+ return line
+
+ @staticmethod
+ def github(problem, filename):
+ line = f'::{problem.level} file={format(filename)},' \
+ f'line={format(problem.line)},col={format(problem.column)}' \
+ f'::{format(problem.line)}:{format(problem.column)} '
+ if problem.rule:
+ line += f'[{problem.rule}] '
+ line += problem.desc
+ return line
+
+
+def show_problems(problems, file, args_format, no_warn):
+ max_level = 0
+ first = True
+
+ if args_format == 'auto':
+ if ('GITHUB_ACTIONS' in os.environ and
+ 'GITHUB_WORKFLOW' in os.environ):
+ args_format = 'github'
+ elif supports_color():
+ args_format = 'colored'
+
+ for problem in problems:
+ max_level = max(max_level, PROBLEM_LEVELS[problem.level])
+ if no_warn and (problem.level != 'error'):
+ continue
+ if args_format == 'parsable':
+ print(Format.parsable(problem, file))
+ elif args_format == 'github':
+ if first:
+ print(f'::group::{file}')
+ first = False
+ print(Format.github(problem, file))
+ elif args_format == 'colored':
+ if first:
+ print(f'\033[4m{file}\033[0m')
+ first = False
+ print(Format.standard_color(problem, file))
+ else:
+ if first:
+ print(file)
+ first = False
+ print(Format.standard(problem, file))
+
+ if not first and args_format == 'github':
+ print('::endgroup::')
+
+ if not first and args_format != 'parsable':
+ print('')
+
+ return max_level
+
+
+def find_project_config_filepath(path='.'):
+ for filename in ('.yamllint', '.yamllint.yaml', '.yamllint.yml'):
+ filepath = os.path.join(path, filename)
+ if os.path.isfile(filepath):
+ return filepath
+
+ if os.path.abspath(path) == os.path.abspath(os.path.expanduser('~')):
+ return None
+ if os.path.abspath(path) == os.path.abspath(os.path.join(path, '..')):
+ return None
+ return find_project_config_filepath(path=os.path.join(path, '..'))
+
+
+def run(argv=None):
+ parser = argparse.ArgumentParser(prog=APP_NAME,
+ description=APP_DESCRIPTION)
+ files_group = parser.add_mutually_exclusive_group(required=True)
+ files_group.add_argument('files', metavar='FILE_OR_DIR', nargs='*',
+ default=(),
+ help='files to check')
+ files_group.add_argument('-', action='store_true', dest='stdin',
+ help='read from standard input')
+ config_group = parser.add_mutually_exclusive_group()
+ config_group.add_argument('-c', '--config-file', dest='config_file',
+ action='store',
+ help='path to a custom configuration')
+ config_group.add_argument('-d', '--config-data', dest='config_data',
+ action='store',
+ help='custom configuration (as YAML source)')
+ parser.add_argument('--list-files', action='store_true', dest='list_files',
+ help='list files to lint and exit')
+ parser.add_argument('-f', '--format',
+ choices=('parsable', 'standard', 'colored', 'github',
+ 'auto'),
+ default='auto', help='format for parsing output')
+ parser.add_argument('-s', '--strict',
+ action='store_true',
+ help='return non-zero exit code on warnings '
+ 'as well as errors')
+ parser.add_argument('--no-warnings',
+ action='store_true',
+ help='output only error level problems')
+ parser.add_argument('-v', '--version', action='version',
+ version=f'{APP_NAME} {APP_VERSION}')
+
+ args = parser.parse_args(argv)
+
+ if 'YAMLLINT_CONFIG_FILE' in os.environ:
+ user_global_config = os.path.expanduser(
+ os.environ['YAMLLINT_CONFIG_FILE'])
+ # User-global config is supposed to be in ~/.config/yamllint/config
+ elif 'XDG_CONFIG_HOME' in os.environ:
+ user_global_config = os.path.join(
+ os.environ['XDG_CONFIG_HOME'], 'yamllint', 'config')
+ else:
+ user_global_config = os.path.expanduser('~/.config/yamllint/config')
+
+ project_config_filepath = find_project_config_filepath()
+ try:
+ if args.config_data is not None:
+ if args.config_data != '' and ':' not in args.config_data:
+ args.config_data = f'extends: {args.config_data}'
+ conf = YamlLintConfig(content=args.config_data)
+ elif args.config_file is not None:
+ conf = YamlLintConfig(file=args.config_file)
+ elif project_config_filepath:
+ conf = YamlLintConfig(file=project_config_filepath)
+ elif os.path.isfile(user_global_config):
+ conf = YamlLintConfig(file=user_global_config)
+ else:
+ conf = YamlLintConfig('extends: default')
+ except YamlLintConfigError as e:
+ print(e, file=sys.stderr)
+ sys.exit(-1)
+
+ if conf.locale is not None:
+ locale.setlocale(locale.LC_ALL, conf.locale)
+
+ if args.list_files:
+ for file in find_files_recursively(args.files, conf):
+ if not conf.is_file_ignored(file):
+ print(file)
+ sys.exit(0)
+
+ max_level = 0
+
+ for file in find_files_recursively(args.files, conf):
+ filepath = file[2:] if file.startswith('./') else file
+ try:
+ with open(file, newline='') as f:
+ problems = linter.run(f, conf, filepath)
+ except OSError as e:
+ print(e, file=sys.stderr)
+ sys.exit(-1)
+ prob_level = show_problems(problems, file, args_format=args.format,
+ no_warn=args.no_warnings)
+ max_level = max(max_level, prob_level)
+
+ # read yaml from stdin
+ if args.stdin:
+ try:
+ problems = linter.run(sys.stdin, conf, '')
+ except OSError as e:
+ print(e, file=sys.stderr)
+ sys.exit(-1)
+ prob_level = show_problems(problems, 'stdin', args_format=args.format,
+ no_warn=args.no_warnings)
+ max_level = max(max_level, prob_level)
+
+ if max_level == PROBLEM_LEVELS['error']:
+ return_code = 1
+ elif max_level == PROBLEM_LEVELS['warning']:
+ return_code = 2 if args.strict else 0
+ else:
+ return_code = 0
+
+ sys.exit(return_code)
diff --git a/yamllint/conf/default.yaml b/yamllint/conf/default.yaml
new file mode 100644
index 0000000..b082e22
--- /dev/null
+++ b/yamllint/conf/default.yaml
@@ -0,0 +1,35 @@
+---
+
+yaml-files:
+ - '*.yaml'
+ - '*.yml'
+ - '.yamllint'
+
+rules:
+ anchors: enable
+ braces: enable
+ brackets: enable
+ colons: enable
+ commas: enable
+ comments:
+ level: warning
+ comments-indentation:
+ level: warning
+ document-end: disable
+ document-start:
+ level: warning
+ empty-lines: enable
+ empty-values: disable
+ float-values: disable
+ hyphens: enable
+ indentation: enable
+ key-duplicates: enable
+ key-ordering: disable
+ line-length: enable
+ new-line-at-end-of-file: enable
+ new-lines: enable
+ octal-values: disable
+ quoted-strings: disable
+ trailing-spaces: enable
+ truthy:
+ level: warning
diff --git a/yamllint/conf/relaxed.yaml b/yamllint/conf/relaxed.yaml
new file mode 100644
index 0000000..83f5340
--- /dev/null
+++ b/yamllint/conf/relaxed.yaml
@@ -0,0 +1,29 @@
+---
+
+extends: default
+
+rules:
+ braces:
+ level: warning
+ max-spaces-inside: 1
+ brackets:
+ level: warning
+ max-spaces-inside: 1
+ colons:
+ level: warning
+ commas:
+ level: warning
+ comments: disable
+ comments-indentation: disable
+ document-start: disable
+ empty-lines:
+ level: warning
+ hyphens:
+ level: warning
+ indentation:
+ level: warning
+ indent-sequences: consistent
+ line-length:
+ level: warning
+ allow-non-breakable-inline-mappings: true
+ truthy: disable
diff --git a/yamllint/config.py b/yamllint/config.py
new file mode 100644
index 0000000..47a61a8
--- /dev/null
+++ b/yamllint/config.py
@@ -0,0 +1,235 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import fileinput
+import os.path
+
+import pathspec
+import yaml
+
+import yamllint.rules
+
+
+class YamlLintConfigError(Exception):
+ pass
+
+
+class YamlLintConfig:
+ def __init__(self, content=None, file=None):
+ assert (content is None) ^ (file is None)
+
+ self.ignore = None
+
+ self.yaml_files = pathspec.PathSpec.from_lines(
+ 'gitwildmatch', ['*.yaml', '*.yml', '.yamllint'])
+
+ self.locale = None
+
+ if file is not None:
+ with open(file) as f:
+ content = f.read()
+
+ self.parse(content)
+ self.validate()
+
+ def is_file_ignored(self, filepath):
+ return self.ignore and self.ignore.match_file(filepath)
+
+ def is_yaml_file(self, filepath):
+ return self.yaml_files.match_file(os.path.basename(filepath))
+
+ def enabled_rules(self, filepath):
+ return [yamllint.rules.get(id) for id, val in self.rules.items()
+ if val is not False and (
+ filepath is None or 'ignore' not in val or
+ not val['ignore'].match_file(filepath))]
+
+ def extend(self, base_config):
+ assert isinstance(base_config, YamlLintConfig)
+
+ for rule in self.rules:
+ if (isinstance(self.rules[rule], dict) and
+ rule in base_config.rules and
+ base_config.rules[rule] is not False):
+ base_config.rules[rule].update(self.rules[rule])
+ else:
+ base_config.rules[rule] = self.rules[rule]
+
+ self.rules = base_config.rules
+
+ if base_config.ignore is not None:
+ self.ignore = base_config.ignore
+
+ def parse(self, raw_content):
+ try:
+ conf = yaml.safe_load(raw_content)
+ except Exception as e:
+ raise YamlLintConfigError(f'invalid config: {e}')
+
+ if not isinstance(conf, dict):
+ raise YamlLintConfigError('invalid config: not a dict')
+
+ self.rules = conf.get('rules', {})
+ for rule in self.rules:
+ if self.rules[rule] == 'enable':
+ self.rules[rule] = {}
+ elif self.rules[rule] == 'disable':
+ self.rules[rule] = False
+
+ # Does this conf override another conf that we need to load?
+ if 'extends' in conf:
+ path = get_extended_config_file(conf['extends'])
+ base = YamlLintConfig(file=path)
+ try:
+ self.extend(base)
+ except Exception as e:
+ raise YamlLintConfigError(f'invalid config: {e}')
+
+ if 'ignore' in conf and 'ignore-from-file' in conf:
+ raise YamlLintConfigError(
+ 'invalid config: ignore and ignore-from-file keys cannot be '
+ 'used together')
+ elif 'ignore-from-file' in conf:
+ if isinstance(conf['ignore-from-file'], str):
+ conf['ignore-from-file'] = [conf['ignore-from-file']]
+ if not (isinstance(conf['ignore-from-file'], list) and all(
+ isinstance(ln, str) for ln in conf['ignore-from-file'])):
+ raise YamlLintConfigError(
+ 'invalid config: ignore-from-file should contain '
+ 'filename(s), either as a list or string')
+ with fileinput.input(conf['ignore-from-file']) as f:
+ self.ignore = pathspec.PathSpec.from_lines('gitwildmatch', f)
+ elif 'ignore' in conf:
+ if isinstance(conf['ignore'], str):
+ self.ignore = pathspec.PathSpec.from_lines(
+ 'gitwildmatch', conf['ignore'].splitlines())
+ elif (isinstance(conf['ignore'], list) and
+ all(isinstance(line, str) for line in conf['ignore'])):
+ self.ignore = pathspec.PathSpec.from_lines(
+ 'gitwildmatch', conf['ignore'])
+ else:
+ raise YamlLintConfigError(
+ 'invalid config: ignore should contain file patterns')
+
+ if 'yaml-files' in conf:
+ if not (isinstance(conf['yaml-files'], list)
+ and all(isinstance(i, str) for i in conf['yaml-files'])):
+ raise YamlLintConfigError(
+ 'invalid config: yaml-files '
+ 'should be a list of file patterns')
+ self.yaml_files = pathspec.PathSpec.from_lines('gitwildmatch',
+ conf['yaml-files'])
+
+ if 'locale' in conf:
+ if not isinstance(conf['locale'], str):
+ raise YamlLintConfigError(
+ 'invalid config: locale should be a string')
+ self.locale = conf['locale']
+
+ def validate(self):
+ for id in self.rules:
+ try:
+ rule = yamllint.rules.get(id)
+ except Exception as e:
+ raise YamlLintConfigError(f'invalid config: {e}')
+
+ self.rules[id] = validate_rule_conf(rule, self.rules[id])
+
+
+def validate_rule_conf(rule, conf):
+ if conf is False: # disable
+ return False
+
+ if isinstance(conf, dict):
+ if ('ignore' in conf and
+ not isinstance(conf['ignore'], pathspec.pathspec.PathSpec)):
+ if isinstance(conf['ignore'], str):
+ conf['ignore'] = pathspec.PathSpec.from_lines(
+ 'gitwildmatch', conf['ignore'].splitlines())
+ elif (isinstance(conf['ignore'], list) and
+ all(isinstance(line, str) for line in conf['ignore'])):
+ conf['ignore'] = pathspec.PathSpec.from_lines(
+ 'gitwildmatch', conf['ignore'])
+ else:
+ raise YamlLintConfigError(
+ 'invalid config: ignore should contain file patterns')
+
+ if 'level' not in conf:
+ conf['level'] = 'error'
+ elif conf['level'] not in ('error', 'warning'):
+ raise YamlLintConfigError(
+ 'invalid config: level should be "error" or "warning"')
+
+ options = getattr(rule, 'CONF', {})
+ options_default = getattr(rule, 'DEFAULT', {})
+ for optkey in conf:
+ if optkey in ('ignore', 'ignore-from-file', 'level'):
+ continue
+ if optkey not in options:
+ raise YamlLintConfigError(
+ f'invalid config: unknown option "{optkey}" for rule '
+ f'"{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(
+ f'invalid config: option "{optkey}" of "{rule.ID}" '
+ f'should be in {options[optkey]}')
+ # Example: CONF = {option: ['flag1', 'flag2', int]}
+ # → {option: [flag1]} → {option: [42, flag1, flag2]}
+ elif isinstance(options[optkey], list):
+ if (type(conf[optkey]) is not list or
+ any(flag not in options[optkey] and
+ type(flag) not in options[optkey]
+ for flag in conf[optkey])):
+ raise YamlLintConfigError(
+ f'invalid config: option "{optkey}" of "{rule.ID}" '
+ f'should only contain values in {options[optkey]}')
+ # Example: CONF = {option: int}
+ # → {option: 42}
+ else:
+ if not isinstance(conf[optkey], options[optkey]):
+ raise YamlLintConfigError(
+ f'invalid config: option "{optkey}" of "{rule.ID}" '
+ f'should be {options[optkey].__name__}')
+ for optkey in options:
+ if optkey not in conf:
+ conf[optkey] = options_default[optkey]
+
+ if hasattr(rule, 'VALIDATE'):
+ res = rule.VALIDATE(conf)
+ if res:
+ raise YamlLintConfigError(f'invalid config: {rule.ID}: {res}')
+ else:
+ raise YamlLintConfigError(
+ f'invalid config: rule "{rule.ID}": should be either "enable", '
+ f'"disable" or a dict')
+
+ return conf
+
+
+def get_extended_config_file(name):
+ # Is it a standard conf shipped with yamllint...
+ if '/' not in name:
+ std_conf = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ 'conf', f'{name}.yaml')
+
+ if os.path.isfile(std_conf):
+ return std_conf
+
+ # or a custom conf on filesystem?
+ return name
diff --git a/yamllint/linter.py b/yamllint/linter.py
new file mode 100644
index 0000000..0de1f71
--- /dev/null
+++ b/yamllint/linter.py
@@ -0,0 +1,236 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import re
+import io
+
+import yaml
+
+from yamllint import parser
+
+
+PROBLEM_LEVELS = {
+ 0: None,
+ 1: 'warning',
+ 2: 'error',
+ None: 0,
+ 'warning': 1,
+ 'error': 2,
+}
+
+DISABLE_RULE_PATTERN = re.compile(r'^# yamllint disable( rule:\S+)*\s*$')
+ENABLE_RULE_PATTERN = re.compile(r'^# yamllint enable( rule:\S+)*\s*$')
+
+
+class LintProblem:
+ """Represents a linting problem found by yamllint."""
+ def __init__(self, line, column, desc='<no description>', rule=None):
+ #: Line on which the problem was found (starting at 1)
+ self.line = line
+ #: Column on which the problem was found (starting at 1)
+ self.column = column
+ #: Human-readable description of the problem
+ self.desc = desc
+ #: Identifier of the rule that detected the problem
+ self.rule = rule
+ self.level = None
+
+ @property
+ def message(self):
+ if self.rule is not None:
+ return f'{self.desc} ({self.rule})'
+ return self.desc
+
+ def __eq__(self, other):
+ return (self.line == other.line and
+ self.column == other.column and
+ self.rule == other.rule)
+
+ def __lt__(self, other):
+ return (self.line < other.line or
+ (self.line == other.line and self.column < other.column))
+
+ def __repr__(self):
+ return f'{self.line}:{self.column}: {self.message}'
+
+
+def get_cosmetic_problems(buffer, conf, filepath):
+ rules = conf.enabled_rules(filepath)
+
+ # Split token rules from line rules
+ token_rules = [r for r in rules if r.TYPE == 'token']
+ comment_rules = [r for r in rules if r.TYPE == 'comment']
+ line_rules = [r for r in rules if r.TYPE == 'line']
+
+ context = {}
+ for rule in token_rules:
+ context[rule.ID] = {}
+
+ class DisableDirective:
+ def __init__(self):
+ self.rules = set()
+ self.all_rules = {r.ID for r in rules}
+
+ def process_comment(self, comment):
+ comment = str(comment)
+
+ if DISABLE_RULE_PATTERN.match(comment):
+ items = comment[18:].rstrip().split(' ')
+ rules = [item[5:] for item in items][1:]
+ if len(rules) == 0:
+ self.rules = self.all_rules.copy()
+ else:
+ for id in rules:
+ if id in self.all_rules:
+ self.rules.add(id)
+
+ elif ENABLE_RULE_PATTERN.match(comment):
+ items = comment[17:].rstrip().split(' ')
+ rules = [item[5:] for item in items][1:]
+ if len(rules) == 0:
+ self.rules.clear()
+ else:
+ for id in rules:
+ self.rules.discard(id)
+
+ def is_disabled_by_directive(self, problem):
+ return problem.rule in self.rules
+
+ class DisableLineDirective(DisableDirective):
+ def process_comment(self, comment):
+ comment = str(comment)
+
+ if re.match(r'^# yamllint disable-line( rule:\S+)*\s*$', comment):
+ items = comment[23:].rstrip().split(' ')
+ rules = [item[5:] for item in items][1:]
+ if len(rules) == 0:
+ self.rules = self.all_rules.copy()
+ else:
+ for id in rules:
+ if id in self.all_rules:
+ self.rules.add(id)
+
+ # Use a cache to store problems and flush it only when an end of line is
+ # found. This allows the use of yamllint directive to disable some rules on
+ # some lines.
+ cache = []
+ disabled = DisableDirective()
+ disabled_for_line = DisableLineDirective()
+ disabled_for_next_line = DisableLineDirective()
+
+ for elem in parser.token_or_comment_or_line_generator(buffer):
+ if isinstance(elem, parser.Token):
+ for rule in token_rules:
+ rule_conf = conf.rules[rule.ID]
+ for problem in rule.check(rule_conf,
+ elem.curr, elem.prev, elem.next,
+ elem.nextnext,
+ context[rule.ID]):
+ problem.rule = rule.ID
+ problem.level = rule_conf['level']
+ cache.append(problem)
+ elif isinstance(elem, parser.Comment):
+ for rule in comment_rules:
+ rule_conf = conf.rules[rule.ID]
+ for problem in rule.check(rule_conf, elem):
+ problem.rule = rule.ID
+ problem.level = rule_conf['level']
+ cache.append(problem)
+
+ disabled.process_comment(elem)
+ if elem.is_inline():
+ disabled_for_line.process_comment(elem)
+ else:
+ disabled_for_next_line.process_comment(elem)
+ elif isinstance(elem, parser.Line):
+ for rule in line_rules:
+ rule_conf = conf.rules[rule.ID]
+ for problem in rule.check(rule_conf, elem):
+ problem.rule = rule.ID
+ problem.level = rule_conf['level']
+ cache.append(problem)
+
+ # This is the last token/comment/line of this line, let's flush the
+ # problems found (but filter them according to the directives)
+ for problem in cache:
+ if not (disabled_for_line.is_disabled_by_directive(problem) or
+ disabled.is_disabled_by_directive(problem)):
+ yield problem
+
+ disabled_for_line = disabled_for_next_line
+ disabled_for_next_line = DisableLineDirective()
+ cache = []
+
+
+def get_syntax_error(buffer):
+ try:
+ list(yaml.parse(buffer, Loader=yaml.BaseLoader))
+ except yaml.error.MarkedYAMLError as e:
+ problem = LintProblem(e.problem_mark.line + 1,
+ e.problem_mark.column + 1,
+ 'syntax error: ' + e.problem + ' (syntax)')
+ problem.level = 'error'
+ return problem
+
+
+def _run(buffer, conf, filepath):
+ assert hasattr(buffer, '__getitem__'), \
+ '_run() argument must be a buffer, not a stream'
+
+ first_line = next(parser.line_generator(buffer)).content
+ if re.match(r'^#\s*yamllint disable-file\s*$', first_line):
+ return
+
+ # If the document contains a syntax error, save it and yield it at the
+ # right line
+ syntax_error = get_syntax_error(buffer)
+
+ for problem in get_cosmetic_problems(buffer, conf, filepath):
+ # Insert the syntax error (if any) at the right place...
+ if (syntax_error and syntax_error.line <= problem.line and
+ syntax_error.column <= problem.column):
+ yield syntax_error
+
+ # Discard the problem since it is at the same place as the syntax
+ # error and is probably redundant (and maybe it's just a 'warning',
+ # in which case the script won't even exit with a failure status).
+ syntax_error = None
+ continue
+
+ yield problem
+
+ if syntax_error:
+ yield syntax_error
+
+
+def run(input, conf, filepath=None):
+ """Lints a YAML source.
+
+ Returns a generator of LintProblem objects.
+
+ :param input: buffer, string or stream to read from
+ :param conf: yamllint configuration object
+ """
+ if filepath is not None and conf.is_file_ignored(filepath):
+ return ()
+
+ if isinstance(input, (bytes, str)):
+ return _run(input, conf, filepath)
+ elif isinstance(input, io.IOBase):
+ # We need to have everything in memory to parse correctly
+ content = input.read()
+ return _run(content, conf, filepath)
+ else:
+ raise TypeError('input should be a string or a stream')
diff --git a/yamllint/parser.py b/yamllint/parser.py
new file mode 100644
index 0000000..f0ee3a6
--- /dev/null
+++ b/yamllint/parser.py
@@ -0,0 +1,159 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import yaml
+
+
+class Line:
+ def __init__(self, line_no, buffer, start, end):
+ self.line_no = line_no
+ self.start = start
+ self.end = end
+ self.buffer = buffer
+
+ @property
+ def content(self):
+ return self.buffer[self.start:self.end]
+
+
+class Token:
+ def __init__(self, line_no, curr, prev, next, nextnext):
+ self.line_no = line_no
+ self.curr = curr
+ self.prev = prev
+ self.next = next
+ self.nextnext = nextnext
+
+
+class Comment:
+ def __init__(self, line_no, column_no, buffer, pointer,
+ token_before=None, token_after=None, comment_before=None):
+ self.line_no = line_no
+ self.column_no = column_no
+ self.buffer = buffer
+ self.pointer = pointer
+ self.token_before = token_before
+ self.token_after = token_after
+ self.comment_before = comment_before
+
+ def __str__(self):
+ end = self.buffer.find('\n', self.pointer)
+ if end == -1:
+ end = self.buffer.find('\0', self.pointer)
+ if end != -1:
+ return self.buffer[self.pointer:end]
+ return self.buffer[self.pointer:]
+
+ def __eq__(self, other):
+ return (isinstance(other, Comment) and
+ self.line_no == other.line_no and
+ self.column_no == other.column_no and
+ str(self) == str(other))
+
+ def is_inline(self):
+ return (
+ not isinstance(self.token_before, yaml.StreamStartToken) and
+ self.line_no == self.token_before.end_mark.line + 1 and
+ # sometimes token end marks are on the next line
+ self.buffer[self.token_before.end_mark.pointer - 1] != '\n'
+ )
+
+
+def line_generator(buffer):
+ line_no = 1
+ cur = 0
+ next = buffer.find('\n')
+ while next != -1:
+ 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
+
+ yield Line(line_no, buffer, start=cur, end=len(buffer))
+
+
+def comments_between_tokens(token1, token2):
+ """Find all comments between two tokens"""
+ if token2 is None:
+ buf = token1.end_mark.buffer[token1.end_mark.pointer:]
+ elif (token1.end_mark.line == token2.start_mark.line and
+ not isinstance(token1, yaml.StreamStartToken) and
+ not isinstance(token2, yaml.StreamEndToken)):
+ return
+ else:
+ buf = token1.end_mark.buffer[token1.end_mark.pointer:
+ token2.start_mark.pointer]
+
+ line_no = token1.end_mark.line + 1
+ column_no = token1.end_mark.column + 1
+ pointer = token1.end_mark.pointer
+
+ comment_before = None
+ for line in buf.split('\n'):
+ pos = line.find('#')
+ if pos != -1:
+ comment = Comment(line_no, column_no + pos,
+ token1.end_mark.buffer, pointer + pos,
+ token1, token2, comment_before)
+ yield comment
+
+ comment_before = comment
+
+ pointer += len(line) + 1
+ line_no += 1
+ column_no = 1
+
+
+def token_or_comment_generator(buffer):
+ yaml_loader = yaml.BaseLoader(buffer)
+
+ try:
+ prev = None
+ curr = yaml_loader.get_token()
+ while curr is not None:
+ next = yaml_loader.get_token()
+ nextnext = (yaml_loader.peek_token()
+ if yaml_loader.check_token() else None)
+
+ yield Token(curr.start_mark.line + 1, curr, prev, next, nextnext)
+
+ yield from comments_between_tokens(curr, next)
+
+ prev = curr
+ curr = next
+
+ except yaml.scanner.ScannerError:
+ pass
+
+
+def token_or_comment_or_line_generator(buffer):
+ """Generator that mixes tokens and lines, ordering them by line number"""
+ tok_or_com_gen = token_or_comment_generator(buffer)
+ line_gen = line_generator(buffer)
+
+ tok_or_com = next(tok_or_com_gen, None)
+ line = next(line_gen, None)
+
+ while tok_or_com is not None or line is not None:
+ if tok_or_com is None or (line is not None and
+ tok_or_com.line_no > line.line_no):
+ yield line
+ line = next(line_gen, None)
+ else:
+ yield tok_or_com
+ tok_or_com = next(tok_or_com_gen, None)
diff --git a/yamllint/rules/__init__.py b/yamllint/rules/__init__.py
new file mode 100644
index 0000000..606b37a
--- /dev/null
+++ b/yamllint/rules/__init__.py
@@ -0,0 +1,73 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from yamllint.rules import (
+ anchors,
+ braces,
+ brackets,
+ colons,
+ commas,
+ comments,
+ comments_indentation,
+ document_end,
+ document_start,
+ empty_lines,
+ empty_values,
+ hyphens,
+ indentation,
+ key_duplicates,
+ key_ordering,
+ line_length,
+ new_line_at_end_of_file,
+ new_lines,
+ octal_values,
+ float_values,
+ quoted_strings,
+ trailing_spaces,
+ truthy,
+)
+
+_RULES = {
+ anchors.ID: anchors,
+ braces.ID: braces,
+ brackets.ID: brackets,
+ colons.ID: colons,
+ commas.ID: commas,
+ comments.ID: comments,
+ comments_indentation.ID: comments_indentation,
+ document_end.ID: document_end,
+ document_start.ID: document_start,
+ empty_lines.ID: empty_lines,
+ empty_values.ID: empty_values,
+ float_values.ID: float_values,
+ 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,
+ octal_values.ID: octal_values,
+ quoted_strings.ID: quoted_strings,
+ trailing_spaces.ID: trailing_spaces,
+ truthy.ID: truthy,
+}
+
+
+def get(id):
+ if id not in _RULES:
+ raise ValueError(f'no such rule: "{id}"')
+
+ return _RULES[id]
diff --git a/yamllint/rules/anchors.py b/yamllint/rules/anchors.py
new file mode 100644
index 0000000..343f38b
--- /dev/null
+++ b/yamllint/rules/anchors.py
@@ -0,0 +1,174 @@
+# Copyright (C) 2023 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to report duplicated anchors and aliases referencing undeclared
+anchors.
+
+.. rubric:: Options
+
+* Set ``forbid-undeclared-aliases`` to ``true`` to avoid aliases that reference
+ an anchor that hasn't been declared (either not declared at all, or declared
+ later in the document).
+* Set ``forbid-duplicated-anchors`` to ``true`` to avoid duplications of a same
+ anchor.
+* Set ``forbid-unused-anchors`` to ``true`` to avoid anchors being declared but
+ not used anywhere in the YAML document via alias.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ anchors:
+ forbid-undeclared-aliases: true
+ forbid-duplicated-anchors: false
+ forbid-unused-anchors: false
+
+.. rubric:: Examples
+
+#. With ``anchors: {forbid-undeclared-aliases: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ ---
+ - &anchor
+ foo: bar
+ - *anchor
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ - &anchor
+ foo: bar
+ - *unknown
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ - &anchor
+ foo: bar
+ - <<: *unknown
+ extra: value
+
+#. With ``anchors: {forbid-duplicated-anchors: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ ---
+ - &anchor1 Foo Bar
+ - &anchor2 [item 1, item 2]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ - &anchor Foo Bar
+ - &anchor [item 1, item 2]
+
+#. With ``anchors: {forbid-unused-anchors: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ ---
+ - &anchor
+ foo: bar
+ - *anchor
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ - &anchor
+ foo: bar
+ - items:
+ - item1
+ - item2
+"""
+
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'anchors'
+TYPE = 'token'
+CONF = {'forbid-undeclared-aliases': bool,
+ 'forbid-duplicated-anchors': bool,
+ 'forbid-unused-anchors': bool}
+DEFAULT = {'forbid-undeclared-aliases': True,
+ 'forbid-duplicated-anchors': False,
+ 'forbid-unused-anchors': False}
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if (conf['forbid-undeclared-aliases'] or
+ conf['forbid-duplicated-anchors'] or
+ conf['forbid-unused-anchors']):
+ if isinstance(token, (
+ yaml.StreamStartToken,
+ yaml.DocumentStartToken,
+ yaml.DocumentEndToken)):
+ context['anchors'] = {}
+
+ if (conf['forbid-undeclared-aliases'] and
+ isinstance(token, yaml.AliasToken) and
+ token.value not in context['anchors']):
+ yield LintProblem(
+ token.start_mark.line + 1, token.start_mark.column + 1,
+ f'found undeclared alias "{token.value}"')
+
+ if (conf['forbid-duplicated-anchors'] and
+ isinstance(token, yaml.AnchorToken) and
+ token.value in context['anchors']):
+ yield LintProblem(
+ token.start_mark.line + 1, token.start_mark.column + 1,
+ f'found duplicated anchor "{token.value}"')
+
+ if conf['forbid-unused-anchors']:
+ # Unused anchors can only be detected at the end of Document.
+ # End of document can be either
+ # - end of stream
+ # - end of document sign '...'
+ # - start of a new document sign '---'
+ # If next token indicates end of document,
+ # check if the anchors have been used or not.
+ # If they haven't been used, report problem on those anchors.
+ if isinstance(next, (yaml.StreamEndToken,
+ yaml.DocumentStartToken,
+ yaml.DocumentEndToken)):
+ for anchor, info in context['anchors'].items():
+ if not info['used']:
+ yield LintProblem(info['line'] + 1,
+ info['column'] + 1,
+ f'found unused anchor "{anchor}"')
+ elif isinstance(token, yaml.AliasToken):
+ context['anchors'].get(token.value, {})['used'] = True
+
+ if (conf['forbid-undeclared-aliases'] or
+ conf['forbid-duplicated-anchors'] or
+ conf['forbid-unused-anchors']):
+ if isinstance(token, yaml.AnchorToken):
+ context['anchors'][token.value] = {
+ 'line': token.start_mark.line,
+ 'column': token.start_mark.column,
+ 'used': False
+ }
diff --git a/yamllint/rules/braces.py b/yamllint/rules/braces.py
new file mode 100644
index 0000000..e77cda4
--- /dev/null
+++ b/yamllint/rules/braces.py
@@ -0,0 +1,201 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to control the use of flow mappings or number of spaces inside
+braces (``{`` and ``}``).
+
+.. rubric:: Options
+
+* ``forbid`` is used to forbid the use of flow mappings which are denoted by
+ surrounding braces (``{`` and ``}``). Use ``true`` to forbid the use of flow
+ mappings completely. Use ``non-empty`` to forbid the use of all flow
+ mappings except for empty ones.
+* ``min-spaces-inside`` defines the minimal number of spaces required inside
+ braces.
+* ``max-spaces-inside`` defines the maximal number of spaces allowed inside
+ braces.
+* ``min-spaces-inside-empty`` defines the minimal number of spaces required
+ inside empty braces.
+* ``max-spaces-inside-empty`` defines the maximal number of spaces allowed
+ inside empty braces.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ braces:
+ forbid: false
+ min-spaces-inside: 0
+ max-spaces-inside: 0
+ min-spaces-inside-empty: -1
+ max-spaces-inside-empty: -1
+
+.. rubric:: Examples
+
+#. With ``braces: {forbid: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object:
+ key1: 4
+ key2: 8
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: { key1: 4, key2: 8 }
+
+#. With ``braces: {forbid: non-empty}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: {}
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: { key1: 4, key2: 8 }
+
+#. With ``braces: {min-spaces-inside: 0, max-spaces-inside: 0}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: {key1: 4, key2: 8}
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: { key1: 4, key2: 8 }
+
+#. With ``braces: {min-spaces-inside: 1, max-spaces-inside: 3}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: { key1: 4, key2: 8 }
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: { key1: 4, key2: 8 }
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: { key1: 4, key2: 8 }
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: {key1: 4, key2: 8 }
+
+#. With ``braces: {min-spaces-inside-empty: 0, max-spaces-inside-empty: 0}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: {}
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: { }
+
+#. With ``braces: {min-spaces-inside-empty: 1, max-spaces-inside-empty: -1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: { }
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: {}
+"""
+
+
+import yaml
+
+from yamllint.linter import LintProblem
+from yamllint.rules.common import spaces_after, spaces_before
+
+
+ID = 'braces'
+TYPE = 'token'
+CONF = {'forbid': (bool, 'non-empty'),
+ 'min-spaces-inside': int,
+ 'max-spaces-inside': int,
+ 'min-spaces-inside-empty': int,
+ 'max-spaces-inside-empty': int}
+DEFAULT = {'forbid': False,
+ 'min-spaces-inside': 0,
+ 'max-spaces-inside': 0,
+ 'min-spaces-inside-empty': -1,
+ 'max-spaces-inside-empty': -1}
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if (conf['forbid'] is True and
+ isinstance(token, yaml.FlowMappingStartToken)):
+ yield LintProblem(token.start_mark.line + 1,
+ token.end_mark.column + 1,
+ 'forbidden flow mapping')
+
+ elif (conf['forbid'] == 'non-empty' and
+ isinstance(token, yaml.FlowMappingStartToken) and
+ not isinstance(next, yaml.FlowMappingEndToken)):
+ yield LintProblem(token.start_mark.line + 1,
+ token.end_mark.column + 1,
+ 'forbidden flow mapping')
+
+ elif (isinstance(token, yaml.FlowMappingStartToken) and
+ isinstance(next, yaml.FlowMappingEndToken)):
+ problem = spaces_after(token, prev, next,
+ min=(conf['min-spaces-inside-empty']
+ if conf['min-spaces-inside-empty'] != -1
+ else conf['min-spaces-inside']),
+ max=(conf['max-spaces-inside-empty']
+ if conf['max-spaces-inside-empty'] != -1
+ else conf['max-spaces-inside']),
+ min_desc='too few spaces inside empty braces',
+ max_desc='too many spaces inside empty braces')
+ if problem is not None:
+ yield problem
+
+ elif isinstance(token, yaml.FlowMappingStartToken):
+ problem = spaces_after(token, prev, next,
+ min=conf['min-spaces-inside'],
+ max=conf['max-spaces-inside'],
+ min_desc='too few spaces inside braces',
+ max_desc='too many spaces inside braces')
+ if problem is not None:
+ yield problem
+
+ elif (isinstance(token, yaml.FlowMappingEndToken) and
+ (prev is None or
+ not isinstance(prev, yaml.FlowMappingStartToken))):
+ problem = spaces_before(token, prev, next,
+ min=conf['min-spaces-inside'],
+ max=conf['max-spaces-inside'],
+ min_desc='too few spaces inside braces',
+ max_desc='too many spaces inside braces')
+ if problem is not None:
+ yield problem
diff --git a/yamllint/rules/brackets.py b/yamllint/rules/brackets.py
new file mode 100644
index 0000000..47d2ad4
--- /dev/null
+++ b/yamllint/rules/brackets.py
@@ -0,0 +1,203 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to control the use of flow sequences or the number of spaces
+inside brackets (``[`` and ``]``).
+
+.. rubric:: Options
+
+* ``forbid`` is used to forbid the use of flow sequences which are denoted by
+ surrounding brackets (``[`` and ``]``). Use ``true`` to forbid the use of
+ flow sequences completely. Use ``non-empty`` to forbid the use of all flow
+ sequences except for empty ones.
+* ``min-spaces-inside`` defines the minimal number of spaces required inside
+ brackets.
+* ``max-spaces-inside`` defines the maximal number of spaces allowed inside
+ brackets.
+* ``min-spaces-inside-empty`` defines the minimal number of spaces required
+ inside empty brackets.
+* ``max-spaces-inside-empty`` defines the maximal number of spaces allowed
+ inside empty brackets.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ brackets:
+ forbid: false
+ min-spaces-inside: 0
+ max-spaces-inside: 0
+ min-spaces-inside-empty: -1
+ max-spaces-inside-empty: -1
+
+.. rubric:: Examples
+
+#. With ``brackets: {forbid: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object:
+ - 1
+ - 2
+ - abc
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: [ 1, 2, abc ]
+
+#. With ``brackets: {forbid: non-empty}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: []
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: [ 1, 2, abc ]
+
+#. With ``brackets: {min-spaces-inside: 0, max-spaces-inside: 0}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: [1, 2, abc]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: [ 1, 2, abc ]
+
+#. With ``brackets: {min-spaces-inside: 1, max-spaces-inside: 3}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: [ 1, 2, abc ]
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: [ 1, 2, abc ]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: [ 1, 2, abc ]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: [1, 2, abc ]
+
+#. With ``brackets: {min-spaces-inside-empty: 0, max-spaces-inside-empty: 0}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: []
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: [ ]
+
+#. With ``brackets: {min-spaces-inside-empty: 1, max-spaces-inside-empty: -1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object: [ ]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: []
+"""
+
+
+import yaml
+
+from yamllint.linter import LintProblem
+from yamllint.rules.common import spaces_after, spaces_before
+
+
+ID = 'brackets'
+TYPE = 'token'
+CONF = {'forbid': (bool, 'non-empty'),
+ 'min-spaces-inside': int,
+ 'max-spaces-inside': int,
+ 'min-spaces-inside-empty': int,
+ 'max-spaces-inside-empty': int}
+DEFAULT = {'forbid': False,
+ 'min-spaces-inside': 0,
+ 'max-spaces-inside': 0,
+ 'min-spaces-inside-empty': -1,
+ 'max-spaces-inside-empty': -1}
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if (conf['forbid'] is True and
+ isinstance(token, yaml.FlowSequenceStartToken)):
+ yield LintProblem(token.start_mark.line + 1,
+ token.end_mark.column + 1,
+ 'forbidden flow sequence')
+
+ elif (conf['forbid'] == 'non-empty' and
+ isinstance(token, yaml.FlowSequenceStartToken) and
+ not isinstance(next, yaml.FlowSequenceEndToken)):
+ yield LintProblem(token.start_mark.line + 1,
+ token.end_mark.column + 1,
+ 'forbidden flow sequence')
+
+ elif (isinstance(token, yaml.FlowSequenceStartToken) and
+ isinstance(next, yaml.FlowSequenceEndToken)):
+ problem = spaces_after(token, prev, next,
+ min=(conf['min-spaces-inside-empty']
+ if conf['min-spaces-inside-empty'] != -1
+ else conf['min-spaces-inside']),
+ max=(conf['max-spaces-inside-empty']
+ if conf['max-spaces-inside-empty'] != -1
+ else conf['max-spaces-inside']),
+ min_desc='too few spaces inside empty brackets',
+ max_desc=('too many spaces inside empty '
+ 'brackets'))
+ if problem is not None:
+ yield problem
+
+ elif isinstance(token, yaml.FlowSequenceStartToken):
+ problem = spaces_after(token, prev, next,
+ min=conf['min-spaces-inside'],
+ max=conf['max-spaces-inside'],
+ min_desc='too few spaces inside brackets',
+ max_desc='too many spaces inside brackets')
+ if problem is not None:
+ yield problem
+
+ elif (isinstance(token, yaml.FlowSequenceEndToken) and
+ (prev is None or
+ not isinstance(prev, yaml.FlowSequenceStartToken))):
+ problem = spaces_before(token, prev, next,
+ min=conf['min-spaces-inside'],
+ max=conf['max-spaces-inside'],
+ min_desc='too few spaces inside brackets',
+ max_desc='too many spaces inside brackets')
+ if problem is not None:
+ yield problem
diff --git a/yamllint/rules/colons.py b/yamllint/rules/colons.py
new file mode 100644
index 0000000..7390e51
--- /dev/null
+++ b/yamllint/rules/colons.py
@@ -0,0 +1,115 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to control the number of spaces before and after colons (``:``).
+
+.. rubric:: Options
+
+* ``max-spaces-before`` defines the maximal number of spaces allowed before
+ colons (use ``-1`` to disable).
+* ``max-spaces-after`` defines the maximal number of spaces allowed after
+ colons (use ``-1`` to disable).
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ colons:
+ max-spaces-before: 0
+ max-spaces-after: 1
+
+.. rubric:: Examples
+
+#. With ``colons: {max-spaces-before: 0, max-spaces-after: 1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object:
+ - a
+ - b
+ key: value
+
+#. With ``colons: {max-spaces-before: 1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ object :
+ - a
+ - b
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object :
+ - a
+ - b
+
+#. With ``colons: {max-spaces-after: 2}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ first: 1
+ second: 2
+ third: 3
+
+ the following code snippet would **FAIL**:
+ ::
+
+ first: 1
+ 2nd: 2
+ third: 3
+"""
+
+
+import yaml
+
+from yamllint.rules.common import is_explicit_key, spaces_after, spaces_before
+
+
+ID = 'colons'
+TYPE = 'token'
+CONF = {'max-spaces-before': int,
+ 'max-spaces-after': int}
+DEFAULT = {'max-spaces-before': 0,
+ 'max-spaces-after': 1}
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if isinstance(token, yaml.ValueToken) and not (
+ isinstance(prev, yaml.AliasToken) and
+ token.start_mark.pointer - prev.end_mark.pointer == 1):
+ problem = spaces_before(token, prev, next,
+ max=conf['max-spaces-before'],
+ max_desc='too many spaces before colon')
+ if problem is not None:
+ yield problem
+
+ problem = spaces_after(token, prev, next,
+ max=conf['max-spaces-after'],
+ max_desc='too many spaces after colon')
+ if problem is not None:
+ yield problem
+
+ if isinstance(token, yaml.KeyToken) and is_explicit_key(token):
+ problem = spaces_after(token, prev, next,
+ max=conf['max-spaces-after'],
+ max_desc='too many spaces after question mark')
+ if problem is not None:
+ yield problem
diff --git a/yamllint/rules/commas.py b/yamllint/rules/commas.py
new file mode 100644
index 0000000..e87c8f9
--- /dev/null
+++ b/yamllint/rules/commas.py
@@ -0,0 +1,140 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to control the number of spaces before and after commas (``,``).
+
+.. rubric:: Options
+
+* ``max-spaces-before`` defines the maximal number of spaces allowed before
+ commas (use ``-1`` to disable).
+* ``min-spaces-after`` defines the minimal number of spaces required after
+ commas.
+* ``max-spaces-after`` defines the maximal number of spaces allowed after
+ commas (use ``-1`` to disable).
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ commas:
+ max-spaces-before: 0
+ min-spaces-after: 1
+ max-spaces-after: 1
+
+.. rubric:: Examples
+
+#. With ``commas: {max-spaces-before: 0}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ strange var:
+ [10, 20, 30, {x: 1, y: 2}]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ strange var:
+ [10, 20 , 30, {x: 1, y: 2}]
+
+#. With ``commas: {max-spaces-before: 2}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ strange var:
+ [10 , 20 , 30, {x: 1 , y: 2}]
+
+#. With ``commas: {max-spaces-before: -1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ strange var:
+ [10,
+ 20 , 30
+ , {x: 1, y: 2}]
+
+#. With ``commas: {min-spaces-after: 1, max-spaces-after: 1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ strange var:
+ [10, 20, 30, {x: 1, y: 2}]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ strange var:
+ [10, 20,30, {x: 1, y: 2}]
+
+#. With ``commas: {min-spaces-after: 1, max-spaces-after: 3}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ strange var:
+ [10, 20, 30, {x: 1, y: 2}]
+
+#. With ``commas: {min-spaces-after: 0, max-spaces-after: 1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ strange var:
+ [10, 20,30, {x: 1, y: 2}]
+"""
+
+
+import yaml
+
+from yamllint.linter import LintProblem
+from yamllint.rules.common import spaces_after, spaces_before
+
+
+ID = 'commas'
+TYPE = 'token'
+CONF = {'max-spaces-before': int,
+ 'min-spaces-after': int,
+ 'max-spaces-after': int}
+DEFAULT = {'max-spaces-before': 0,
+ 'min-spaces-after': 1,
+ 'max-spaces-after': 1}
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if isinstance(token, yaml.FlowEntryToken):
+ if (prev is not None and conf['max-spaces-before'] != -1 and
+ prev.end_mark.line < token.start_mark.line):
+ yield LintProblem(token.start_mark.line + 1,
+ max(1, token.start_mark.column),
+ 'too many spaces before comma')
+ else:
+ problem = spaces_before(token, prev, next,
+ max=conf['max-spaces-before'],
+ max_desc='too many spaces before comma')
+ if problem is not None:
+ yield problem
+
+ problem = spaces_after(token, prev, next,
+ min=conf['min-spaces-after'],
+ max=conf['max-spaces-after'],
+ min_desc='too few spaces after comma',
+ max_desc='too many spaces after comma')
+ if problem is not None:
+ yield problem
diff --git a/yamllint/rules/comments.py b/yamllint/rules/comments.py
new file mode 100644
index 0000000..1259dea
--- /dev/null
+++ b/yamllint/rules/comments.py
@@ -0,0 +1,113 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to control the position and formatting of comments.
+
+.. rubric:: Options
+
+* Use ``require-starting-space`` to require a space character right after the
+ ``#``. Set to ``true`` to enable, ``false`` to disable.
+* Use ``ignore-shebangs`` to ignore a
+ `shebang <https://en.wikipedia.org/wiki/Shebang_(Unix)>`_ at the beginning of
+ the file when ``require-starting-space`` is set.
+* ``min-spaces-from-content`` is used to visually separate inline comments from
+ content. It defines the minimal required number of spaces between a comment
+ and its preceding content.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ comments:
+ require-starting-space: true
+ ignore-shebangs: true
+ min-spaces-from-content: 2
+
+.. rubric:: Examples
+
+#. With ``comments: {require-starting-space: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ # This sentence
+ # is a block comment
+
+ the following code snippet would **PASS**:
+ ::
+
+ ##############################
+ ## This is some documentation
+
+ the following code snippet would **FAIL**:
+ ::
+
+ #This sentence
+ #is a block comment
+
+#. With ``comments: {min-spaces-from-content: 2}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ x = 2 ^ 127 - 1 # Mersenne prime number
+
+ the following code snippet would **FAIL**:
+ ::
+
+ x = 2 ^ 127 - 1 # Mersenne prime number
+"""
+
+
+from yamllint.linter import LintProblem
+
+
+ID = 'comments'
+TYPE = 'comment'
+CONF = {'require-starting-space': bool,
+ 'ignore-shebangs': bool,
+ 'min-spaces-from-content': int}
+DEFAULT = {'require-starting-space': True,
+ 'ignore-shebangs': True,
+ 'min-spaces-from-content': 2}
+
+
+def check(conf, comment):
+ if (conf['min-spaces-from-content'] != -1 and comment.is_inline() and
+ comment.pointer - comment.token_before.end_mark.pointer <
+ conf['min-spaces-from-content']):
+ yield LintProblem(comment.line_no, comment.column_no,
+ 'too few spaces before comment')
+
+ if conf['require-starting-space']:
+ text_start = comment.pointer + 1
+ while (comment.buffer[text_start] == '#' and
+ text_start < len(comment.buffer)):
+ text_start += 1
+ if text_start < len(comment.buffer):
+ if (conf['ignore-shebangs'] and
+ comment.line_no == 1 and
+ comment.column_no == 1 and
+ comment.buffer[text_start] == '!'):
+ return
+ # We can test for both \r and \r\n just by checking first char
+ # \r itself is a valid newline on some older OS.
+ elif comment.buffer[text_start] not in {' ', '\n', '\r', '\x00'}:
+ column = comment.column_no + text_start - comment.pointer
+ yield LintProblem(comment.line_no,
+ column,
+ 'missing starting space in comment')
diff --git a/yamllint/rules/comments_indentation.py b/yamllint/rules/comments_indentation.py
new file mode 100644
index 0000000..569abee
--- /dev/null
+++ b/yamllint/rules/comments_indentation.py
@@ -0,0 +1,137 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to force comments to be indented like content.
+
+.. rubric:: Examples
+
+#. With ``comments-indentation: {}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ # Fibonacci
+ [0, 1, 1, 2, 3, 5]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ # Fibonacci
+ [0, 1, 1, 2, 3, 5]
+
+ the following code snippet would **PASS**:
+ ::
+
+ list:
+ - 2
+ - 3
+ # - 4
+ - 5
+
+ the following code snippet would **FAIL**:
+ ::
+
+ list:
+ - 2
+ - 3
+ # - 4
+ - 5
+
+ the following code snippet would **PASS**:
+ ::
+
+ # This is the first object
+ obj1:
+ - item A
+ # - item B
+ # This is the second object
+ obj2: []
+
+ the following code snippet would **PASS**:
+ ::
+
+ # This sentence
+ # is a block comment
+
+ the following code snippet would **FAIL**:
+ ::
+
+ # This sentence
+ # is a block comment
+"""
+
+
+import yaml
+
+from yamllint.linter import LintProblem
+from yamllint.rules.common import get_line_indent
+
+
+ID = 'comments-indentation'
+TYPE = 'comment'
+
+
+# Case A:
+#
+# prev: line:
+# # commented line
+# current: line
+#
+# Case B:
+#
+# prev: line
+# # commented line 1
+# # commented line 2
+# current: line
+
+def check(conf, comment):
+ # Only check block comments
+ if (not isinstance(comment.token_before, yaml.StreamStartToken) and
+ comment.token_before.end_mark.line + 1 == comment.line_no):
+ return
+
+ next_line_indent = comment.token_after.start_mark.column
+ if isinstance(comment.token_after, yaml.StreamEndToken):
+ next_line_indent = 0
+
+ if isinstance(comment.token_before, yaml.StreamStartToken):
+ prev_line_indent = 0
+ else:
+ prev_line_indent = get_line_indent(comment.token_before)
+
+ # In the following case only the next line indent is valid:
+ # list:
+ # # comment
+ # - 1
+ # - 2
+ prev_line_indent = max(prev_line_indent, next_line_indent)
+
+ # If two indents are valid but a previous comment went back to normal
+ # indent, for the next ones to do the same. In other words, avoid this:
+ # list:
+ # - 1
+ # # comment on valid indent (0)
+ # # comment on valid indent (4)
+ # other-list:
+ # - 2
+ if (comment.comment_before is not None and
+ not comment.comment_before.is_inline()):
+ prev_line_indent = comment.comment_before.column_no - 1
+
+ if (comment.column_no - 1 != prev_line_indent and
+ comment.column_no - 1 != next_line_indent):
+ yield LintProblem(comment.line_no, comment.column_no,
+ 'comment not indented like content')
diff --git a/yamllint/rules/common.py b/yamllint/rules/common.py
new file mode 100644
index 0000000..06f560c
--- /dev/null
+++ b/yamllint/rules/common.py
@@ -0,0 +1,88 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import string
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+def spaces_after(token, prev, next, min=-1, max=-1,
+ min_desc=None, max_desc=None):
+ if next is not None and token.end_mark.line == next.start_mark.line:
+ spaces = next.start_mark.pointer - token.end_mark.pointer
+ if max != - 1 and spaces > max:
+ return LintProblem(token.start_mark.line + 1,
+ next.start_mark.column, max_desc)
+ elif min != - 1 and spaces < min:
+ return LintProblem(token.start_mark.line + 1,
+ next.start_mark.column + 1, min_desc)
+
+
+def spaces_before(token, prev, next, min=-1, max=-1,
+ min_desc=None, max_desc=None):
+ if (prev is not None and prev.end_mark.line == token.start_mark.line and
+ # Discard tokens (only scalars?) that end at the start of next line
+ (prev.end_mark.pointer == 0 or
+ prev.end_mark.buffer[prev.end_mark.pointer - 1] != '\n')):
+ spaces = token.start_mark.pointer - prev.end_mark.pointer
+ if max != - 1 and spaces > max:
+ return LintProblem(token.start_mark.line + 1,
+ token.start_mark.column, max_desc)
+ elif min != - 1 and spaces < min:
+ return LintProblem(token.start_mark.line + 1,
+ token.start_mark.column + 1, min_desc)
+
+
+def get_line_indent(token):
+ """Finds the indent of the line the token starts in."""
+ start = token.start_mark.buffer.rfind('\n', 0,
+ token.start_mark.pointer) + 1
+ content = start
+ while token.start_mark.buffer[content] == ' ':
+ content += 1
+ return content - start
+
+
+def get_real_end_line(token):
+ """Finds the line on which the token really ends.
+
+ With pyyaml, scalar tokens often end on a next line.
+ """
+ end_line = token.end_mark.line + 1
+
+ if not isinstance(token, yaml.ScalarToken):
+ return end_line
+
+ pos = token.end_mark.pointer - 1
+ while (pos >= token.start_mark.pointer - 1 and
+ token.end_mark.buffer[pos] in string.whitespace):
+ if token.end_mark.buffer[pos] == '\n':
+ end_line -= 1
+ pos -= 1
+ return end_line
+
+
+def is_explicit_key(token):
+ # explicit key:
+ # ? key
+ # : v
+ # or
+ # ?
+ # key
+ # : v
+ return (token.start_mark.pointer < token.end_mark.pointer and
+ token.start_mark.buffer[token.start_mark.pointer] == '?')
diff --git a/yamllint/rules/document_end.py b/yamllint/rules/document_end.py
new file mode 100644
index 0000000..2337484
--- /dev/null
+++ b/yamllint/rules/document_end.py
@@ -0,0 +1,116 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to require or forbid the use of document end marker (``...``).
+
+.. rubric:: Options
+
+* Set ``present`` to ``true`` when the document end marker is required, or to
+ ``false`` when it is forbidden.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ document-end:
+ present: true
+
+.. rubric:: Examples
+
+#. With ``document-end: {present: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ ---
+ this:
+ is: [a, document]
+ ...
+ ---
+ - this
+ - is: another one
+ ...
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ this:
+ is: [a, document]
+ ---
+ - this
+ - is: another one
+ ...
+
+#. With ``document-end: {present: false}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ ---
+ this:
+ is: [a, document]
+ ---
+ - this
+ - is: another one
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ this:
+ is: [a, document]
+ ...
+ ---
+ - this
+ - is: another one
+"""
+
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'document-end'
+TYPE = 'token'
+CONF = {'present': bool}
+DEFAULT = {'present': True}
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if conf['present']:
+ is_stream_end = isinstance(token, yaml.StreamEndToken)
+ is_start = isinstance(token, yaml.DocumentStartToken)
+ prev_is_end_or_stream_start = isinstance(
+ prev, (yaml.DocumentEndToken, yaml.StreamStartToken)
+ )
+ prev_is_directive = isinstance(prev, yaml.DirectiveToken)
+
+ if is_stream_end and not prev_is_end_or_stream_start:
+ yield LintProblem(token.start_mark.line, 1,
+ 'missing document end "..."')
+ elif is_start and not (prev_is_end_or_stream_start
+ or prev_is_directive):
+ yield LintProblem(token.start_mark.line + 1, 1,
+ 'missing document end "..."')
+
+ else:
+ if isinstance(token, yaml.DocumentEndToken):
+ yield LintProblem(token.start_mark.line + 1,
+ token.start_mark.column + 1,
+ 'found forbidden document end "..."')
diff --git a/yamllint/rules/document_start.py b/yamllint/rules/document_start.py
new file mode 100644
index 0000000..f1d6667
--- /dev/null
+++ b/yamllint/rules/document_start.py
@@ -0,0 +1,100 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to require or forbid the use of document start marker (``---``).
+
+.. rubric:: Options
+
+* Set ``present`` to ``true`` when the document start marker is required, or to
+ ``false`` when it is forbidden.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ document-start:
+ present: true
+
+.. rubric:: Examples
+
+#. With ``document-start: {present: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ ---
+ this:
+ is: [a, document]
+ ---
+ - this
+ - is: another one
+
+ the following code snippet would **FAIL**:
+ ::
+
+ this:
+ is: [a, document]
+ ---
+ - this
+ - is: another one
+
+#. With ``document-start: {present: false}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ this:
+ is: [a, document]
+ ...
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ this:
+ is: [a, document]
+ ...
+"""
+
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'document-start'
+TYPE = 'token'
+CONF = {'present': bool}
+DEFAULT = {'present': True}
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if conf['present']:
+ if (isinstance(prev, (yaml.StreamStartToken,
+ yaml.DocumentEndToken,
+ yaml.DirectiveToken)) and
+ not isinstance(token, (yaml.DocumentStartToken,
+ yaml.DirectiveToken,
+ yaml.StreamEndToken))):
+ yield LintProblem(token.start_mark.line + 1, 1,
+ 'missing document start "---"')
+
+ else:
+ if isinstance(token, yaml.DocumentStartToken):
+ yield LintProblem(token.start_mark.line + 1,
+ token.start_mark.column + 1,
+ 'found forbidden document start "---"')
diff --git a/yamllint/rules/empty_lines.py b/yamllint/rules/empty_lines.py
new file mode 100644
index 0000000..eca7812
--- /dev/null
+++ b/yamllint/rules/empty_lines.py
@@ -0,0 +1,117 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to set a maximal number of allowed consecutive blank lines.
+
+.. rubric:: Options
+
+* ``max`` defines the maximal number of empty lines allowed in the document.
+* ``max-start`` defines the maximal number of empty lines allowed at the
+ beginning of the file. This option takes precedence over ``max``.
+* ``max-end`` defines the maximal number of empty lines allowed at the end of
+ the file. This option takes precedence over ``max``.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ empty-lines:
+ max: 2
+ max-start: 0
+ max-end: 0
+
+.. rubric:: Examples
+
+#. With ``empty-lines: {max: 1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ - foo:
+ - 1
+ - 2
+
+ - bar: [3, 4]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ - foo:
+ - 1
+ - 2
+
+
+ - bar: [3, 4]
+"""
+
+
+from yamllint.linter import LintProblem
+
+
+ID = 'empty-lines'
+TYPE = 'line'
+CONF = {'max': int,
+ 'max-start': int,
+ 'max-end': int}
+DEFAULT = {'max': 2,
+ 'max-start': 0,
+ 'max-end': 0}
+
+
+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 + 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
+
+ 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 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') 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
+
+ max = conf['max-end']
+
+ if blank_lines > max:
+ yield LintProblem(line.line_no, 1,
+ f'too many blank lines ({blank_lines} > {max})')
diff --git a/yamllint/rules/empty_values.py b/yamllint/rules/empty_values.py
new file mode 100644
index 0000000..6c8328b
--- /dev/null
+++ b/yamllint/rules/empty_values.py
@@ -0,0 +1,140 @@
+# Copyright (C) 2017 Greg Dubicki
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to prevent nodes with empty content, that implicitly result in
+``null`` values.
+
+.. rubric:: Options
+
+* Use ``forbid-in-block-mappings`` to prevent empty values in block mappings.
+* Use ``forbid-in-flow-mappings`` to prevent empty values in flow mappings.
+* Use ``forbid-in-block-sequences`` to prevent empty values in block sequences.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ empty-values:
+ forbid-in-block-mappings: true
+ forbid-in-flow-mappings: true
+ forbid-in-block-sequences: true
+
+.. rubric:: Examples
+
+#. With ``empty-values: {forbid-in-block-mappings: true}``
+
+ the following code snippets would **PASS**:
+ ::
+
+ some-mapping:
+ sub-element: correctly indented
+
+ ::
+
+ explicitly-null: null
+
+ the following code snippets would **FAIL**:
+ ::
+
+ some-mapping:
+ sub-element: incorrectly indented
+
+ ::
+
+ implicitly-null:
+
+#. With ``empty-values: {forbid-in-flow-mappings: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ {prop: null}
+ {a: 1, b: 2, c: 3}
+
+ the following code snippets would **FAIL**:
+ ::
+
+ {prop: }
+
+ ::
+
+ {a: 1, b:, c: 3}
+
+#. With ``empty-values: {forbid-in-block-sequences: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ some-sequence:
+ - string item
+
+ ::
+
+ some-sequence:
+ - null
+
+ the following code snippets would **FAIL**:
+ ::
+
+ some-sequence:
+ -
+
+ ::
+
+ some-sequence:
+ - string item
+ -
+
+"""
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'empty-values'
+TYPE = 'token'
+CONF = {'forbid-in-block-mappings': bool,
+ 'forbid-in-flow-mappings': bool,
+ 'forbid-in-block-sequences': bool}
+DEFAULT = {'forbid-in-block-mappings': True,
+ 'forbid-in-flow-mappings': True,
+ 'forbid-in-block-sequences': True}
+
+
+def check(conf, token, prev, next, nextnext, context):
+
+ if conf['forbid-in-block-mappings']:
+ if isinstance(token, yaml.ValueToken) and isinstance(next, (
+ yaml.KeyToken, yaml.BlockEndToken)):
+ yield LintProblem(token.start_mark.line + 1,
+ token.end_mark.column + 1,
+ 'empty value in block mapping')
+
+ if conf['forbid-in-flow-mappings']:
+ if isinstance(token, yaml.ValueToken) and isinstance(next, (
+ yaml.FlowEntryToken, yaml.FlowMappingEndToken)):
+ yield LintProblem(token.start_mark.line + 1,
+ token.end_mark.column + 1,
+ 'empty value in flow mapping')
+
+ if conf['forbid-in-block-sequences']:
+ if isinstance(token, yaml.BlockEntryToken) and isinstance(next, (
+ yaml.KeyToken, yaml.BlockEndToken, yaml.BlockEntryToken)):
+ yield LintProblem(token.start_mark.line + 1,
+ token.end_mark.column + 1,
+ 'empty value in block sequence')
diff --git a/yamllint/rules/float_values.py b/yamllint/rules/float_values.py
new file mode 100644
index 0000000..a5852c5
--- /dev/null
+++ b/yamllint/rules/float_values.py
@@ -0,0 +1,158 @@
+# Copyright (C) 2022 the yamllint contributors
+
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to limit the permitted values for floating-point numbers.
+YAML permits three classes of float expressions: approximation to real numbers,
+positive and negative infinity and "not a number".
+
+.. rubric:: Options
+
+* Use ``require-numeral-before-decimal`` to require floats to start
+ with a numeral (ex ``0.0`` instead of ``.0``).
+* Use ``forbid-scientific-notation`` to forbid scientific notation.
+* Use ``forbid-nan`` to forbid NaN (not a number) values.
+* Use ``forbid-inf`` to forbid infinite values.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ float-values:
+ forbid-inf: false
+ forbid-nan: false
+ forbid-scientific-notation: false
+ require-numeral-before-decimal: false
+
+.. rubric:: Examples
+
+#. With ``float-values: {require-numeral-before-decimal: true}``
+
+ the following code snippets would **PASS**:
+ ::
+
+ anemometer:
+ angle: 0.0
+
+ the following code snippets would **FAIL**:
+ ::
+
+ anemometer:
+ angle: .0
+
+#. With ``float-values: {forbid-scientific-notation: true}``
+
+ the following code snippets would **PASS**:
+ ::
+
+ anemometer:
+ angle: 0.00001
+
+ the following code snippets would **FAIL**:
+ ::
+
+ anemometer:
+ angle: 10e-6
+
+#. With ``float-values: {forbid-nan: true}``
+
+ the following code snippets would **FAIL**:
+ ::
+
+ anemometer:
+ angle: .NaN
+
+ #. With ``float-values: {forbid-inf: true}``
+
+ the following code snippets would **FAIL**:
+ ::
+
+ anemometer:
+ angle: .inf
+"""
+
+import re
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'float-values'
+TYPE = 'token'
+CONF = {
+ 'require-numeral-before-decimal': bool,
+ 'forbid-scientific-notation': bool,
+ 'forbid-nan': bool,
+ 'forbid-inf': bool,
+}
+DEFAULT = {
+ 'require-numeral-before-decimal': False,
+ 'forbid-scientific-notation': False,
+ 'forbid-nan': False,
+ 'forbid-inf': False,
+}
+
+IS_NUMERAL_BEFORE_DECIMAL_PATTERN = (
+ re.compile('[-+]?(\\.[0-9]+)([eE][-+]?[0-9]+)?$')
+)
+IS_SCIENTIFIC_NOTATION_PATTERN = re.compile(
+ '[-+]?(\\.[0-9]+|[0-9]+(\\.[0-9]*)?)([eE][-+]?[0-9]+)$'
+)
+IS_INF_PATTERN = re.compile('[-+]?(\\.inf|\\.Inf|\\.INF)$')
+IS_NAN_PATTERN = re.compile('(\\.nan|\\.NaN|\\.NAN)$')
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if prev and isinstance(prev, yaml.tokens.TagToken):
+ return
+ if not isinstance(token, yaml.tokens.ScalarToken):
+ return
+ if token.style:
+ return
+ val = token.value
+
+ if conf['forbid-nan'] and IS_NAN_PATTERN.match(val):
+ yield LintProblem(
+ token.start_mark.line + 1,
+ token.start_mark.column + 1,
+ f'forbidden not a number value "{token.value}"',
+ )
+
+ if conf['forbid-inf'] and IS_INF_PATTERN.match(val):
+ yield LintProblem(
+ token.start_mark.line + 1,
+ token.start_mark.column + 1,
+ f'forbidden infinite value "{token.value}"',
+ )
+
+ if conf[
+ 'forbid-scientific-notation'
+ ] and IS_SCIENTIFIC_NOTATION_PATTERN.match(val):
+ yield LintProblem(
+ token.start_mark.line + 1,
+ token.start_mark.column + 1,
+ f'forbidden scientific notation "{token.value}"',
+ )
+
+ if conf[
+ 'require-numeral-before-decimal'
+ ] and IS_NUMERAL_BEFORE_DECIMAL_PATTERN.match(val):
+ yield LintProblem(
+ token.start_mark.line + 1,
+ token.start_mark.column + 1,
+ f'forbidden decimal missing 0 prefix "{token.value}"',
+ )
diff --git a/yamllint/rules/hyphens.py b/yamllint/rules/hyphens.py
new file mode 100644
index 0000000..50e4d6d
--- /dev/null
+++ b/yamllint/rules/hyphens.py
@@ -0,0 +1,95 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to control the number of spaces after hyphens (``-``).
+
+.. rubric:: Options
+
+* ``max-spaces-after`` defines the maximal number of spaces allowed after
+ hyphens.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ hyphens:
+ max-spaces-after: 1
+
+.. rubric:: Examples
+
+#. With ``hyphens: {max-spaces-after: 1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ - first list:
+ - a
+ - b
+ - - 1
+ - 2
+ - 3
+
+ the following code snippet would **FAIL**:
+ ::
+
+ - first list:
+ - a
+ - b
+
+ the following code snippet would **FAIL**:
+ ::
+
+ - - 1
+ - 2
+ - 3
+
+#. With ``hyphens: {max-spaces-after: 3}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ - key
+ - key2
+ - key42
+
+ the following code snippet would **FAIL**:
+ ::
+
+ - key
+ - key2
+ - key42
+"""
+
+
+import yaml
+
+from yamllint.rules.common import spaces_after
+
+
+ID = 'hyphens'
+TYPE = 'token'
+CONF = {'max-spaces-after': int}
+DEFAULT = {'max-spaces-after': 1}
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if isinstance(token, yaml.BlockEntryToken):
+ problem = spaces_after(token, prev, next,
+ max=conf['max-spaces-after'],
+ max_desc='too many spaces after hyphen')
+ if problem is not None:
+ yield problem
diff --git a/yamllint/rules/indentation.py b/yamllint/rules/indentation.py
new file mode 100644
index 0000000..d839d5a
--- /dev/null
+++ b/yamllint/rules/indentation.py
@@ -0,0 +1,587 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to control the indentation.
+
+.. rubric:: Options
+
+* ``spaces`` defines the indentation width, in spaces. Set either to an integer
+ (e.g. ``2`` or ``4``, representing the number of spaces in an indentation
+ level) or to ``consistent`` to allow any number, as long as it remains the
+ same within the file.
+* ``indent-sequences`` defines whether block sequences should be indented or
+ not (when in a mapping, this indentation is not mandatory -- some people
+ perceive the ``-`` as part of the indentation). Possible values: ``true``,
+ ``false``, ``whatever`` and ``consistent``. ``consistent`` requires either
+ all block sequences to be indented, or none to be. ``whatever`` means either
+ indenting or not indenting individual block sequences is OK.
+* ``check-multi-line-strings`` defines whether to lint indentation in
+ multi-line strings. Set to ``true`` to enable, ``false`` to disable.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ indentation:
+ spaces: consistent
+ indent-sequences: true
+ check-multi-line-strings: false
+
+.. rubric:: Examples
+
+#. With ``indentation: {spaces: 1}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ history:
+ - name: Unix
+ date: 1969
+ - name: Linux
+ date: 1991
+ nest:
+ recurse:
+ - haystack:
+ needle
+
+#. With ``indentation: {spaces: 4}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ history:
+ - name: Unix
+ date: 1969
+ - name: Linux
+ date: 1991
+ nest:
+ recurse:
+ - haystack:
+ needle
+
+ the following code snippet would **FAIL**:
+ ::
+
+ history:
+ - name: Unix
+ date: 1969
+ - name: Linux
+ date: 1991
+ nest:
+ recurse:
+ - haystack:
+ needle
+
+#. With ``indentation: {spaces: consistent}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ history:
+ - name: Unix
+ date: 1969
+ - name: Linux
+ date: 1991
+ nest:
+ recurse:
+ - haystack:
+ needle
+
+ the following code snippet would **FAIL**:
+ ::
+
+ some:
+ Russian:
+ dolls
+
+#. With ``indentation: {spaces: 2, indent-sequences: false}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ list:
+ - flying
+ - spaghetti
+ - monster
+
+ the following code snippet would **FAIL**:
+ ::
+
+ list:
+ - flying
+ - spaghetti
+ - monster
+
+#. With ``indentation: {spaces: 2, indent-sequences: whatever}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ list:
+ - flying:
+ - spaghetti
+ - monster
+ - not flying:
+ - spaghetti
+ - sauce
+
+#. With ``indentation: {spaces: 2, indent-sequences: consistent}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ - flying:
+ - spaghetti
+ - monster
+ - not flying:
+ - spaghetti
+ - sauce
+
+ the following code snippet would **FAIL**:
+ ::
+
+ - flying:
+ - spaghetti
+ - monster
+ - not flying:
+ - spaghetti
+ - sauce
+
+#. With ``indentation: {spaces: 4, check-multi-line-strings: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ Blaise Pascal:
+ Je vous écris une longue lettre parce que
+ je n'ai pas le temps d'en écrire une courte.
+
+ the following code snippet would **PASS**:
+ ::
+
+ Blaise Pascal: Je vous écris une longue lettre parce que
+ je n'ai pas le temps d'en écrire une courte.
+
+ the following code snippet would **FAIL**:
+ ::
+
+ Blaise Pascal: Je vous écris une longue lettre parce que
+ je n'ai pas le temps d'en écrire une courte.
+
+ the following code snippet would **FAIL**:
+ ::
+
+ C code:
+ void main() {
+ printf("foo");
+ }
+
+ the following code snippet would **PASS**:
+ ::
+
+ C code:
+ void main() {
+ printf("bar");
+ }
+"""
+
+import yaml
+
+from yamllint.linter import LintProblem
+from yamllint.rules.common import get_real_end_line, is_explicit_key
+
+
+ID = 'indentation'
+TYPE = 'token'
+CONF = {'spaces': (int, 'consistent'),
+ 'indent-sequences': (bool, 'whatever', 'consistent'),
+ 'check-multi-line-strings': bool}
+DEFAULT = {'spaces': 'consistent',
+ 'indent-sequences': True,
+ 'check-multi-line-strings': False}
+
+ROOT, B_MAP, F_MAP, B_SEQ, F_SEQ, B_ENT, KEY, VAL = range(8)
+labels = ('ROOT', 'B_MAP', 'F_MAP', 'B_SEQ', 'F_SEQ', 'B_ENT', 'KEY', 'VAL')
+
+
+class Parent:
+ def __init__(self, type, indent, line_indent=None):
+ self.type = type
+ self.indent = indent
+ self.line_indent = line_indent
+ self.explicit_key = False
+ self.implicit_block_seq = False
+
+ def __repr__(self):
+ return f'{labels[self.type]}:{self.indent}'
+
+
+def check_scalar_indentation(conf, token, context):
+ if token.start_mark.line == token.end_mark.line:
+ return
+
+ def compute_expected_indent(found_indent):
+ def detect_indent(base_indent):
+ if not isinstance(context['spaces'], int):
+ context['spaces'] = found_indent - base_indent
+ return base_indent + context['spaces']
+
+ if token.plain:
+ return token.start_mark.column
+ elif token.style in ('"', "'"):
+ return token.start_mark.column + 1
+ elif token.style in ('>', '|'):
+ if context['stack'][-1].type == B_ENT:
+ # - >
+ # multi
+ # line
+ return detect_indent(token.start_mark.column)
+ elif context['stack'][-1].type == KEY:
+ assert context['stack'][-1].explicit_key
+ # - ? >
+ # multi-line
+ # key
+ # : >
+ # multi-line
+ # value
+ return detect_indent(token.start_mark.column)
+ elif context['stack'][-1].type == VAL:
+ if token.start_mark.line + 1 > context['cur_line']:
+ # - key:
+ # >
+ # multi
+ # line
+ return detect_indent(context['stack'][-1].indent)
+ elif context['stack'][-2].explicit_key:
+ # - ? key
+ # : >
+ # multi-line
+ # value
+ return detect_indent(token.start_mark.column)
+ else:
+ # - key: >
+ # multi
+ # line
+ return detect_indent(context['stack'][-2].indent)
+ else:
+ return detect_indent(context['stack'][-1].indent)
+
+ expected_indent = None
+
+ line_no = token.start_mark.line + 1
+
+ line_start = token.start_mark.pointer
+ while True:
+ line_start = token.start_mark.buffer.find(
+ '\n', line_start, token.end_mark.pointer - 1) + 1
+ if line_start == 0:
+ break
+ line_no += 1
+
+ indent = 0
+ while token.start_mark.buffer[line_start + indent] == ' ':
+ indent += 1
+ if token.start_mark.buffer[line_start + indent] == '\n':
+ continue
+
+ if expected_indent is None:
+ expected_indent = compute_expected_indent(indent)
+
+ if indent != expected_indent:
+ yield LintProblem(line_no, indent + 1,
+ f'wrong indentation: expected {expected_indent}'
+ f'but found {indent}')
+
+
+def _check(conf, token, prev, next, nextnext, context):
+ if 'stack' not in context:
+ context['stack'] = [Parent(ROOT, 0)]
+ context['cur_line'] = -1
+ context['spaces'] = conf['spaces']
+ context['indent-sequences'] = conf['indent-sequences']
+
+ # Step 1: Lint
+
+ is_visible = (
+ not isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)) and
+ not isinstance(token, yaml.BlockEndToken) and
+ not (isinstance(token, yaml.ScalarToken) and token.value == ''))
+ first_in_line = (is_visible and
+ token.start_mark.line + 1 > context['cur_line'])
+
+ def detect_indent(base_indent, next):
+ if not isinstance(context['spaces'], int):
+ context['spaces'] = next.start_mark.column - base_indent
+ return base_indent + context['spaces']
+
+ if first_in_line:
+ found_indentation = token.start_mark.column
+ expected = context['stack'][-1].indent
+
+ if isinstance(token, (yaml.FlowMappingEndToken,
+ yaml.FlowSequenceEndToken)):
+ expected = context['stack'][-1].line_indent
+ elif (context['stack'][-1].type == KEY and
+ context['stack'][-1].explicit_key and
+ not isinstance(token, yaml.ValueToken)):
+ expected = detect_indent(expected, token)
+
+ if found_indentation != expected:
+ if expected < 0:
+ message = f'wrong indentation: expected at least ' \
+ f'{found_indentation + 1}'
+ else:
+ message = f'wrong indentation: expected {expected} but ' \
+ f'found {found_indentation}'
+ yield LintProblem(token.start_mark.line + 1,
+ found_indentation + 1, message)
+
+ if (isinstance(token, yaml.ScalarToken) and
+ conf['check-multi-line-strings']):
+ yield from check_scalar_indentation(conf, token, context)
+
+ # Step 2.a:
+
+ if is_visible:
+ context['cur_line'] = get_real_end_line(token)
+ if first_in_line:
+ context['cur_line_indent'] = found_indentation
+
+ # Step 2.b: Update state
+
+ if isinstance(token, yaml.BlockMappingStartToken):
+ # - a: 1
+ # or
+ # - ? a
+ # : 1
+ # or
+ # - ?
+ # a
+ # : 1
+ assert isinstance(next, yaml.KeyToken)
+ assert next.start_mark.line == token.start_mark.line
+
+ indent = token.start_mark.column
+
+ context['stack'].append(Parent(B_MAP, indent))
+
+ elif isinstance(token, yaml.FlowMappingStartToken):
+ if next.start_mark.line == token.start_mark.line:
+ # - {a: 1, b: 2}
+ indent = next.start_mark.column
+ else:
+ # - {
+ # a: 1, b: 2
+ # }
+ indent = detect_indent(context['cur_line_indent'], next)
+
+ context['stack'].append(Parent(F_MAP, indent,
+ line_indent=context['cur_line_indent']))
+
+ elif isinstance(token, yaml.BlockSequenceStartToken):
+ # - - a
+ # - b
+ assert isinstance(next, yaml.BlockEntryToken)
+ assert next.start_mark.line == token.start_mark.line
+
+ indent = token.start_mark.column
+
+ context['stack'].append(Parent(B_SEQ, indent))
+
+ elif (isinstance(token, yaml.BlockEntryToken) and
+ # in case of an empty entry
+ not isinstance(next, (yaml.BlockEntryToken, yaml.BlockEndToken))):
+ # It looks like pyyaml doesn't issue BlockSequenceStartTokens when the
+ # list is not indented. We need to compensate that.
+ if context['stack'][-1].type != B_SEQ:
+ context['stack'].append(Parent(B_SEQ, token.start_mark.column))
+ context['stack'][-1].implicit_block_seq = True
+
+ if next.start_mark.line == token.end_mark.line:
+ # - 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
+ # -
+ # key:
+ # value
+ indent = detect_indent(token.start_mark.column, next)
+
+ context['stack'].append(Parent(B_ENT, indent))
+
+ elif isinstance(token, yaml.FlowSequenceStartToken):
+ if next.start_mark.line == token.start_mark.line:
+ # - [a, b]
+ indent = next.start_mark.column
+ else:
+ # - [
+ # a, b
+ # ]
+ indent = detect_indent(context['cur_line_indent'], next)
+
+ context['stack'].append(Parent(F_SEQ, indent,
+ line_indent=context['cur_line_indent']))
+
+ elif isinstance(token, yaml.KeyToken):
+ indent = context['stack'][-1].indent
+
+ context['stack'].append(Parent(KEY, indent))
+
+ context['stack'][-1].explicit_key = is_explicit_key(token)
+
+ elif isinstance(token, yaml.ValueToken):
+ assert context['stack'][-1].type == KEY
+
+ # Special cases:
+ # key: &anchor
+ # value
+ # and:
+ # key: !!tag
+ # value
+ if isinstance(next, (yaml.AnchorToken, yaml.TagToken)):
+ if (next.start_mark.line == prev.start_mark.line and
+ next.start_mark.line < nextnext.start_mark.line):
+ next = nextnext
+
+ # Only if value is not empty
+ if not isinstance(next, (yaml.BlockEndToken,
+ yaml.FlowMappingEndToken,
+ yaml.FlowSequenceEndToken,
+ yaml.KeyToken)):
+ if context['stack'][-1].explicit_key:
+ # ? k
+ # : value
+ # or
+ # ? k
+ # :
+ # value
+ indent = detect_indent(context['stack'][-1].indent, next)
+ elif next.start_mark.line == prev.start_mark.line:
+ # k: value
+ indent = next.start_mark.column
+ elif isinstance(next, (yaml.BlockSequenceStartToken,
+ yaml.BlockEntryToken)):
+ # NOTE: We add BlockEntryToken in the test above because
+ # sometimes BlockSequenceStartToken are not issued. Try
+ # yaml.scan()ning this:
+ # '- lib:\n'
+ # ' - var\n'
+ if context['indent-sequences'] is False:
+ indent = context['stack'][-1].indent
+ elif context['indent-sequences'] is True:
+ if (context['spaces'] == 'consistent' and
+ next.start_mark.column -
+ context['stack'][-1].indent == 0):
+ # In this case, the block sequence item is not indented
+ # (while it should be), but we don't know yet the
+ # indentation it should have (because `spaces` is
+ # `consistent` and its value has not been computed yet
+ # -- this is probably the beginning of the document).
+ # So we choose an unknown value (-1).
+ indent = -1
+ else:
+ indent = detect_indent(context['stack'][-1].indent,
+ next)
+ else: # 'whatever' or 'consistent'
+ if next.start_mark.column == context['stack'][-1].indent:
+ # key:
+ # - e1
+ # - e2
+ if context['indent-sequences'] == 'consistent':
+ context['indent-sequences'] = False
+ indent = context['stack'][-1].indent
+ else:
+ if context['indent-sequences'] == 'consistent':
+ context['indent-sequences'] = True
+ # key:
+ # - e1
+ # - e2
+ indent = detect_indent(context['stack'][-1].indent,
+ next)
+ else:
+ # k:
+ # value
+ indent = detect_indent(context['stack'][-1].indent, next)
+
+ context['stack'].append(Parent(VAL, indent))
+
+ consumed_current_token = False
+ while True:
+ if (context['stack'][-1].type == F_SEQ and
+ isinstance(token, yaml.FlowSequenceEndToken) and
+ not consumed_current_token):
+ context['stack'].pop()
+ consumed_current_token = True
+
+ elif (context['stack'][-1].type == F_MAP and
+ isinstance(token, yaml.FlowMappingEndToken) and
+ not consumed_current_token):
+ context['stack'].pop()
+ consumed_current_token = True
+
+ elif (context['stack'][-1].type in (B_MAP, B_SEQ) and
+ isinstance(token, yaml.BlockEndToken) and
+ not context['stack'][-1].implicit_block_seq and
+ not consumed_current_token):
+ context['stack'].pop()
+ consumed_current_token = True
+
+ elif (context['stack'][-1].type == B_ENT and
+ not isinstance(token, yaml.BlockEntryToken) and
+ context['stack'][-2].implicit_block_seq and
+ not isinstance(token, (yaml.AnchorToken, yaml.TagToken)) and
+ not isinstance(next, yaml.BlockEntryToken)):
+ context['stack'].pop()
+ context['stack'].pop()
+
+ elif (context['stack'][-1].type == B_ENT and
+ isinstance(next, (yaml.BlockEntryToken, yaml.BlockEndToken))):
+ context['stack'].pop()
+
+ elif (context['stack'][-1].type == VAL and
+ not isinstance(token, yaml.ValueToken) and
+ not isinstance(token, (yaml.AnchorToken, yaml.TagToken))):
+ assert context['stack'][-2].type == KEY
+ context['stack'].pop()
+ context['stack'].pop()
+
+ elif (context['stack'][-1].type == KEY and
+ isinstance(next, (yaml.BlockEndToken,
+ yaml.FlowMappingEndToken,
+ yaml.FlowSequenceEndToken,
+ yaml.KeyToken))):
+ # A key without a value: it's part of a set. Let's drop this key
+ # and leave room for the next one.
+ context['stack'].pop()
+
+ else:
+ break
+
+
+def check(conf, token, prev, next, nextnext, context):
+ try:
+ yield from _check(conf, token, prev, next, nextnext, context)
+ except AssertionError:
+ yield LintProblem(token.start_mark.line + 1,
+ token.start_mark.column + 1,
+ 'cannot infer indentation: unexpected token')
diff --git a/yamllint/rules/key_duplicates.py b/yamllint/rules/key_duplicates.py
new file mode 100644
index 0000000..771a8e2
--- /dev/null
+++ b/yamllint/rules/key_duplicates.py
@@ -0,0 +1,100 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+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'
+
+MAP, SEQ = range(2)
+
+
+class Parent:
+ 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)):
+ if len(context['stack']) > 0:
+ 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 and
+ # `<<` is "merge key", see http://yaml.org/type/merge.html
+ next.value != '<<'):
+ yield LintProblem(
+ next.start_mark.line + 1, next.start_mark.column + 1,
+ f'duplication of key "{next.value}" in mapping')
+ else:
+ context['stack'][-1].keys.append(next.value)
diff --git a/yamllint/rules/key_ordering.py b/yamllint/rules/key_ordering.py
new file mode 100644
index 0000000..7fa9597
--- /dev/null
+++ b/yamllint/rules/key_ordering.py
@@ -0,0 +1,127 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to enforce alphabetical ordering of keys in mappings. 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 ``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
+
+#. With global option ``locale: "en_US.UTF-8"`` and rule ``key-ordering: {}``
+
+ as opposed to before, the following code snippet would now **PASS**:
+ ::
+
+ - t-shirt: 1
+ T-shirt: 2
+ t-shirts: 3
+ T-shirts: 4
+ - hair: true
+ haïr: true
+ hais: true
+ haïssable: true
+"""
+
+from locale import strcoll
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'key-ordering'
+TYPE = 'token'
+
+MAP, SEQ = range(2)
+
+
+class Parent:
+ 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(strcoll(next.value, key) < 0
+ for key in context['stack'][-1].keys):
+ yield LintProblem(
+ next.start_mark.line + 1, next.start_mark.column + 1,
+ f'wrong ordering of key "{next.value}" in mapping')
+ else:
+ context['stack'][-1].keys.append(next.value)
diff --git a/yamllint/rules/line_length.py b/yamllint/rules/line_length.py
new file mode 100644
index 0000000..e7cc8bc
--- /dev/null
+++ b/yamllint/rules/line_length.py
@@ -0,0 +1,158 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to set a limit to lines length.
+
+.. rubric:: Options
+
+* ``max`` defines the maximal (inclusive) length of lines.
+* ``allow-non-breakable-words`` is used to allow non breakable words (without
+ spaces inside) to overflow the limit. This is useful for long URLs, for
+ instance. Use ``true`` to allow, ``false`` to forbid.
+* ``allow-non-breakable-inline-mappings`` implies ``allow-non-breakable-words``
+ and extends it to also allow non-breakable words in inline mappings.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ line-length:
+ max: 80
+ allow-non-breakable-words: true
+ allow-non-breakable-inline-mappings: false
+
+.. rubric:: Examples
+
+#. With ``line-length: {max: 70}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ long sentence:
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+ eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+ the following code snippet would **FAIL**:
+ ::
+
+ long sentence:
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+ tempor incididunt ut labore et dolore magna aliqua.
+
+#. With ``line-length: {max: 60, allow-non-breakable-words: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ this:
+ is:
+ - a:
+ http://localhost/very/very/very/very/very/very/very/very/long/url
+
+ # this comment is too long,
+ # but hard to split:
+ # http://localhost/another/very/very/very/very/very/very/very/very/long/url
+
+ the following code snippet would **FAIL**:
+ ::
+
+ - this line is waaaaaaaaaaaaaay too long but could be easily split...
+
+ and the following code snippet would also **FAIL**:
+ ::
+
+ - foobar: http://localhost/very/very/very/very/very/very/very/very/long/url
+
+#. With ``line-length: {max: 60, allow-non-breakable-words: true,
+ allow-non-breakable-inline-mappings: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ - foobar: http://localhost/very/very/very/very/very/very/very/very/long/url
+
+#. With ``line-length: {max: 60, allow-non-breakable-words: false}``
+
+ the following code snippet would **FAIL**:
+ ::
+
+ this:
+ is:
+ - a:
+ http://localhost/very/very/very/very/very/very/very/very/long/url
+"""
+
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'line-length'
+TYPE = 'line'
+CONF = {'max': int,
+ 'allow-non-breakable-words': bool,
+ 'allow-non-breakable-inline-mappings': bool}
+DEFAULT = {'max': 80,
+ 'allow-non-breakable-words': True,
+ 'allow-non-breakable-inline-mappings': False}
+
+
+def check_inline_mapping(line):
+ loader = yaml.SafeLoader(line.content)
+ try:
+ while loader.peek_token():
+ if isinstance(loader.get_token(), yaml.BlockMappingStartToken):
+ while loader.peek_token():
+ if isinstance(loader.get_token(), yaml.ValueToken):
+ t = loader.get_token()
+ if isinstance(t, yaml.ScalarToken):
+ return (
+ ' ' not in line.content[t.start_mark.column:])
+ except yaml.scanner.ScannerError:
+ pass
+
+ return False
+
+
+def check(conf, line):
+ if line.end - line.start > conf['max']:
+ conf['allow-non-breakable-words'] |= \
+ conf['allow-non-breakable-inline-mappings']
+ if conf['allow-non-breakable-words']:
+ start = line.start
+ while start < line.end and line.buffer[start] == ' ':
+ start += 1
+
+ if start != line.end:
+ if line.buffer[start] == '#':
+ while line.buffer[start] == '#':
+ start += 1
+ start += 1
+ elif line.buffer[start] == '-':
+ start += 2
+
+ if line.buffer.find(' ', start, line.end) == -1:
+ return
+
+ if (conf['allow-non-breakable-inline-mappings'] and
+ check_inline_mapping(line)):
+ return
+
+ yield LintProblem(line.line_no, conf['max'] + 1,
+ 'line too long (%d > %d characters)' %
+ (line.end - line.start, conf['max']))
diff --git a/yamllint/rules/new_line_at_end_of_file.py b/yamllint/rules/new_line_at_end_of_file.py
new file mode 100644
index 0000000..302cfe6
--- /dev/null
+++ b/yamllint/rules/new_line_at_end_of_file.py
@@ -0,0 +1,36 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to require a new line character (``\\n``) at the end of files.
+
+The POSIX standard `requires the last line to end with a new line character
+<https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_206>`_.
+All UNIX tools expect a new line at the end of files. Most text editors use
+this convention too.
+"""
+
+
+from yamllint.linter import LintProblem
+
+
+ID = 'new-line-at-end-of-file'
+TYPE = 'line'
+
+
+def check(conf, line):
+ if line.end == len(line.buffer) and line.end > line.start:
+ yield LintProblem(line.line_no, line.end - line.start + 1,
+ 'no new line character at the end of file')
diff --git a/yamllint/rules/new_lines.py b/yamllint/rules/new_lines.py
new file mode 100644
index 0000000..b3f018a
--- /dev/null
+++ b/yamllint/rules/new_lines.py
@@ -0,0 +1,59 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to force the type of new line characters.
+
+.. rubric:: Options
+
+* Set ``type`` to ``unix`` to enforce UNIX-typed new line characters (``\\n``),
+ set ``type`` to ``dos`` to enforce DOS-typed new line characters
+ (``\\r\\n``), or set ``type`` to ``platform`` to infer the type from the
+ system running yamllint (``\\n`` on POSIX / UNIX / Linux / Mac OS systems or
+ ``\\r\\n`` on DOS / Windows systems).
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ new-lines:
+ type: unix
+"""
+
+from os import linesep
+
+from yamllint.linter import LintProblem
+
+
+ID = 'new-lines'
+TYPE = 'line'
+CONF = {'type': ('unix', 'dos', 'platform')}
+DEFAULT = {'type': 'unix'}
+
+
+def check(conf, line):
+ if conf['type'] == 'unix':
+ newline_char = '\n'
+ elif conf['type'] == 'platform':
+ newline_char = linesep
+ elif conf['type'] == 'dos':
+ newline_char = '\r\n'
+
+ if line.start == 0 and len(line.buffer) > line.end:
+ if line.buffer[line.end:line.end + len(newline_char)] != newline_char:
+ yield LintProblem(1, line.end - line.start + 1,
+ 'wrong new line character: expected {}'
+ .format(repr(newline_char).strip('\'')))
diff --git a/yamllint/rules/octal_values.py b/yamllint/rules/octal_values.py
new file mode 100644
index 0000000..eb24c81
--- /dev/null
+++ b/yamllint/rules/octal_values.py
@@ -0,0 +1,112 @@
+# Copyright (C) 2017 ScienJus
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to prevent values with octal numbers. In YAML, numbers that
+start with ``0`` are interpreted as octal, but this is not always wanted.
+For instance ``010`` is the city code of Beijing, and should not be
+converted to ``8``.
+
+.. rubric:: Options
+
+* Use ``forbid-implicit-octal`` to prevent numbers starting with ``0``.
+* Use ``forbid-explicit-octal`` to prevent numbers starting with ``0o``.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ octal-values:
+ forbid-implicit-octal: true
+ forbid-explicit-octal: true
+
+.. rubric:: Examples
+
+#. With ``octal-values: {forbid-implicit-octal: true}``
+
+ the following code snippets would **PASS**:
+ ::
+
+ user:
+ city-code: '010'
+
+ the following code snippets would **PASS**:
+ ::
+
+ user:
+ city-code: 010,021
+
+ the following code snippets would **FAIL**:
+ ::
+
+ user:
+ city-code: 010
+
+#. With ``octal-values: {forbid-explicit-octal: true}``
+
+ the following code snippets would **PASS**:
+ ::
+
+ user:
+ city-code: '0o10'
+
+ the following code snippets would **FAIL**:
+ ::
+
+ user:
+ city-code: 0o10
+"""
+
+import re
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'octal-values'
+TYPE = 'token'
+CONF = {'forbid-implicit-octal': bool,
+ 'forbid-explicit-octal': bool}
+DEFAULT = {'forbid-implicit-octal': True,
+ 'forbid-explicit-octal': True}
+
+IS_OCTAL_NUMBER_PATTERN = re.compile(r'^[0-7]+$')
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if prev and isinstance(prev, yaml.tokens.TagToken):
+ return
+
+ if conf['forbid-implicit-octal']:
+ if isinstance(token, yaml.tokens.ScalarToken):
+ if not token.style:
+ val = token.value
+ if (val.isdigit() and len(val) > 1 and val[0] == '0' and
+ IS_OCTAL_NUMBER_PATTERN.match(val[1:])):
+ yield LintProblem(
+ token.start_mark.line + 1, token.end_mark.column + 1,
+ f'forbidden implicit octal value "{token.value}"')
+
+ if conf['forbid-explicit-octal']:
+ if isinstance(token, yaml.tokens.ScalarToken):
+ if not token.style:
+ val = token.value
+ if (len(val) > 2 and val[:2] == '0o' and
+ IS_OCTAL_NUMBER_PATTERN.match(val[2:])):
+ yield LintProblem(
+ token.start_mark.line + 1, token.end_mark.column + 1,
+ f'forbidden explicit octal value "{token.value}"')
diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py
new file mode 100644
index 0000000..9380ae5
--- /dev/null
+++ b/yamllint/rules/quoted_strings.py
@@ -0,0 +1,289 @@
+# Copyright (C) 2018 ClearScore
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to forbid any string values that are not quoted, or to prevent
+quoted strings without needing it. You can also enforce the type of the quote
+used.
+
+.. rubric:: Options
+
+* ``quote-type`` defines allowed quotes: ``single``, ``double`` or ``any``
+ (default).
+* ``required`` defines whether using quotes in string values is required
+ (``true``, default) or not (``false``), or only allowed when really needed
+ (``only-when-needed``).
+* ``extra-required`` is a list of PCRE regexes to force string values to be
+ quoted, if they match any regex. This option can only be used with
+ ``required: false`` and ``required: only-when-needed``.
+* ``extra-allowed`` is a list of PCRE regexes to allow quoted string values,
+ even if ``required: only-when-needed`` is set.
+* ``allow-quoted-quotes`` allows (``true``) using disallowed quotes for strings
+ with allowed quotes inside. Default ``false``.
+
+**Note**: Multi-line strings (with ``|`` or ``>``) will not be checked.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ quoted-strings:
+ quote-type: any
+ required: true
+ extra-required: []
+ extra-allowed: []
+ allow-quoted-quotes: false
+
+.. rubric:: Examples
+
+#. With ``quoted-strings: {quote-type: any, required: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ foo: "bar"
+ bar: 'foo'
+ number: 123
+ boolean: true
+
+ the following code snippet would **FAIL**:
+ ::
+
+ foo: bar
+
+#. With ``quoted-strings: {quote-type: single, required: only-when-needed}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ foo: bar
+ bar: foo
+ not_number: '123'
+ not_boolean: 'true'
+ not_comment: '# comment'
+ not_list: '[1, 2, 3]'
+ not_map: '{a: 1, b: 2}'
+
+ the following code snippet would **FAIL**:
+ ::
+
+ foo: 'bar'
+
+#. With ``quoted-strings: {required: false, extra-required: [^http://,
+ ^ftp://]}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ - localhost
+ - "localhost"
+ - "http://localhost"
+ - "ftp://localhost"
+
+ the following code snippet would **FAIL**:
+ ::
+
+ - http://localhost
+ - ftp://localhost
+
+#. With ``quoted-strings: {required: only-when-needed, extra-allowed:
+ [^http://, ^ftp://], extra-required: [QUOTED]}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ - localhost
+ - "http://localhost"
+ - "ftp://localhost"
+ - "this is a string that needs to be QUOTED"
+
+ the following code snippet would **FAIL**:
+ ::
+
+ - "localhost"
+ - this is a string that needs to be QUOTED
+
+#. With ``quoted-strings: {quote-type: double, allow-quoted-quotes: false}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ foo: "bar\\"baz"
+
+ the following code snippet would **FAIL**:
+ ::
+
+ foo: 'bar"baz'
+
+#. With ``quoted-strings: {quote-type: double, allow-quoted-quotes: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ foo: 'bar"baz'
+
+"""
+
+import re
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+ID = 'quoted-strings'
+TYPE = 'token'
+CONF = {'quote-type': ('any', 'single', 'double'),
+ 'required': (True, False, 'only-when-needed'),
+ 'extra-required': [str],
+ 'extra-allowed': [str],
+ 'allow-quoted-quotes': bool}
+DEFAULT = {'quote-type': 'any',
+ 'required': True,
+ 'extra-required': [],
+ 'extra-allowed': [],
+ 'allow-quoted-quotes': False}
+
+
+def VALIDATE(conf):
+ if conf['required'] is True and len(conf['extra-allowed']) > 0:
+ return 'cannot use both "required: true" and "extra-allowed"'
+ if conf['required'] is True and len(conf['extra-required']) > 0:
+ return 'cannot use both "required: true" and "extra-required"'
+ if conf['required'] is False and len(conf['extra-allowed']) > 0:
+ return 'cannot use both "required: false" and "extra-allowed"'
+
+
+DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str'
+
+# https://stackoverflow.com/a/36514274
+yaml.resolver.Resolver.add_implicit_resolver(
+ 'tag:yaml.org,2002:int',
+ re.compile(r'''^(?:[-+]?0b[0-1_]+
+ |[-+]?0o?[0-7_]+
+ |[-+]?0[0-7_]+
+ |[-+]?(?:0|[1-9][0-9_]*)
+ |[-+]?0x[0-9a-fA-F_]+
+ |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.VERBOSE),
+ list('-+0123456789'))
+
+
+def _quote_match(quote_type, token_style):
+ return ((quote_type == 'any') or
+ (quote_type == 'single' and token_style == "'") or
+ (quote_type == 'double' and token_style == '"'))
+
+
+def _quotes_are_needed(string):
+ loader = yaml.BaseLoader('key: ' + string)
+ # Remove the 5 first tokens corresponding to 'key: ' (StreamStartToken,
+ # BlockMappingStartToken, KeyToken, ScalarToken(value=key), ValueToken)
+ for _ in range(5):
+ loader.get_token()
+ try:
+ a, b = loader.get_token(), loader.get_token()
+ if (isinstance(a, yaml.ScalarToken) and a.style is None and
+ isinstance(b, yaml.BlockEndToken) and a.value == string):
+ return False
+ return True
+ except yaml.scanner.ScannerError:
+ return True
+
+
+def _has_quoted_quotes(token):
+ return ((not token.plain) and
+ ((token.style == "'" and '"' in token.value) or
+ (token.style == '"' and "'" in token.value)))
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if not (isinstance(token, yaml.tokens.ScalarToken) and
+ isinstance(prev, (yaml.BlockEntryToken, yaml.FlowEntryToken,
+ yaml.FlowSequenceStartToken, yaml.TagToken,
+ yaml.ValueToken))):
+
+ return
+
+ # Ignore explicit types, e.g. !!str testtest or !!int 42
+ if (prev and isinstance(prev, yaml.tokens.TagToken) and
+ prev.value[0] == '!!'):
+ return
+
+ # Ignore numbers, booleans, etc.
+ resolver = yaml.resolver.Resolver()
+ tag = resolver.resolve(yaml.nodes.ScalarNode, token.value, (True, False))
+ if token.plain and tag != DEFAULT_SCALAR_TAG:
+ return
+
+ # Ignore multi-line strings
+ if not token.plain and token.style in ("|", ">"):
+ return
+
+ quote_type = conf['quote-type']
+
+ msg = None
+ if conf['required'] is True:
+
+ # Quotes are mandatory and need to match config
+ if (token.style is None or
+ not (_quote_match(quote_type, token.style) or
+ (conf['allow-quoted-quotes'] and _has_quoted_quotes(token)))):
+ msg = f"string value is not quoted with {quote_type} quotes"
+
+ elif conf['required'] is False:
+
+ # Quotes are not mandatory but when used need to match config
+ if (token.style and
+ not _quote_match(quote_type, token.style) and
+ not (conf['allow-quoted-quotes'] and
+ _has_quoted_quotes(token))):
+ msg = f"string value is not quoted with {quote_type} quotes"
+
+ elif not token.style:
+ is_extra_required = any(re.search(r, token.value)
+ for r in conf['extra-required'])
+ if is_extra_required:
+ msg = "string value is not quoted"
+
+ elif conf['required'] == 'only-when-needed':
+
+ # Quotes are not strictly needed here
+ if (token.style and tag == DEFAULT_SCALAR_TAG and token.value and
+ not _quotes_are_needed(token.value)):
+ is_extra_required = any(re.search(r, token.value)
+ for r in conf['extra-required'])
+ is_extra_allowed = any(re.search(r, token.value)
+ for r in conf['extra-allowed'])
+ if not (is_extra_required or is_extra_allowed):
+ msg = f"string value is redundantly quoted with " \
+ f"{quote_type} quotes"
+
+ # But when used need to match config
+ elif (token.style and
+ not _quote_match(quote_type, token.style) and
+ not (conf['allow-quoted-quotes'] and _has_quoted_quotes(token))):
+ msg = f"string value is not quoted with {quote_type} quotes"
+
+ elif not token.style:
+ is_extra_required = len(conf['extra-required']) and any(
+ re.search(r, token.value) for r in conf['extra-required'])
+ if is_extra_required:
+ msg = "string value is not quoted"
+
+ if msg is not None:
+ yield LintProblem(
+ token.start_mark.line + 1,
+ token.start_mark.column + 1,
+ msg)
diff --git a/yamllint/rules/trailing_spaces.py b/yamllint/rules/trailing_spaces.py
new file mode 100644
index 0000000..2295714
--- /dev/null
+++ b/yamllint/rules/trailing_spaces.py
@@ -0,0 +1,61 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to forbid trailing spaces at the end of lines.
+
+.. rubric:: Examples
+
+#. With ``trailing-spaces: {}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ this document doesn't contain
+ any trailing
+ spaces
+
+ the following code snippet would **FAIL**:
+ ::
+
+ this document contains """ """
+ trailing spaces
+ on lines 1 and 3 """ """
+"""
+
+
+import string
+
+from yamllint.linter import LintProblem
+
+
+ID = 'trailing-spaces'
+TYPE = 'line'
+
+
+def check(conf, line):
+ if line.end == 0:
+ return
+
+ # YAML recognizes two white space characters: space and tab.
+ # http://yaml.org/spec/1.2/spec.html#id2775170
+
+ pos = line.end
+ while line.buffer[pos - 1] in string.whitespace and pos > line.start:
+ pos -= 1
+
+ if pos != line.end and line.buffer[pos] in ' \t':
+ yield LintProblem(line.line_no, pos - line.start + 1,
+ 'trailing spaces')
diff --git a/yamllint/rules/truthy.py b/yamllint/rules/truthy.py
new file mode 100644
index 0000000..d19f6ea
--- /dev/null
+++ b/yamllint/rules/truthy.py
@@ -0,0 +1,157 @@
+# Copyright (C) 2016 Peter Ericson
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+Use this rule to forbid non-explicitly 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'``.
+* ``check-keys`` disables verification for keys in mappings. By default,
+ ``truthy`` rule applies to both keys and values. Set this option to ``false``
+ to prevent this.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ truthy:
+ allowed-values: ['true', 'false']
+ check-keys: true
+
+.. rubric:: Examples
+
+#. With ``truthy: {}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ boolean: true
+
+ object: {"True": 1, 1: "True"}
+
+ "yes": 1
+ "on": 2
+ "True": 3
+
+ explicit:
+ string1: !!str True
+ string2: !!str yes
+ string3: !!str off
+ encoded: !!binary |
+ True
+ OFF
+ pad== # this decodes as 'N\xbb\x9e8Qii'
+ boolean1: !!bool true
+ boolean2: !!bool "false"
+ boolean3: !!bool FALSE
+ boolean4: !!bool True
+ boolean5: !!bool off
+ boolean6: !!bool NO
+
+ the following code snippet would **FAIL**:
+ ::
+
+ object: {True: 1, 1: True}
+
+ the following code snippet would **FAIL**:
+ ::
+
+ 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
+
+#. With ``truthy: {check-keys: false}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ yes: 1
+ on: 2
+ true: 3
+
+ the following code snippet would **FAIL**:
+ ::
+
+ yes: Yes
+ on: On
+ true: True
+"""
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+TRUTHY = ['YES', 'Yes', 'yes',
+ 'NO', 'No', 'no',
+ 'TRUE', 'True', 'true',
+ 'FALSE', 'False', 'false',
+ 'ON', 'On', 'on',
+ 'OFF', 'Off', 'off']
+
+
+ID = 'truthy'
+TYPE = 'token'
+CONF = {'allowed-values': TRUTHY.copy(), 'check-keys': bool}
+DEFAULT = {'allowed-values': ['true', 'false'], 'check-keys': True}
+
+
+def check(conf, token, prev, next, nextnext, context):
+ if prev and isinstance(prev, yaml.tokens.TagToken):
+ return
+
+ if (not conf['check-keys'] and isinstance(prev, yaml.tokens.KeyToken) and
+ isinstance(token, yaml.tokens.ScalarToken)):
+ return
+
+ if isinstance(token, yaml.tokens.ScalarToken):
+ 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 one of [" +
+ ", ".join(sorted(conf['allowed-values'])) + "]")