diff options
219 files changed, 16836 insertions, 0 deletions
@@ -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. @@ -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 Binary files differnew file mode 100644 index 0000000..6bad6c0 --- /dev/null +++ b/docs/screenshot.png 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'])) + "]") |