Compare commits

...

49 Commits

Author SHA1 Message Date
Adrien Vergé
387d14f816 yamllint version 0.5.0 2016-01-22 19:10:05 +01:00
Adrien Vergé
ba8a9d0ba1 Doc: Give an explicit link from configuration to rules 2016-01-22 18:42:03 +01:00
Adrien Vergé
26b5364be4 Doc: Add Read The Docs status badge in README 2016-01-22 18:20:31 +01:00
Adrien Vergé
47d6534e75 Doc: Write the configuration page 2016-01-22 18:20:31 +01:00
Adrien Vergé
237db5aeef Doc: Document how to use the yamllint Python module 2016-01-22 18:20:31 +01:00
Adrien Vergé
6e9de02eac Doc: Update index
Add a brief description and remove unused links.
2016-01-22 18:20:31 +01:00
Adrien Vergé
044c049462 Doc: Document rules 2016-01-22 18:20:31 +01:00
Adrien Vergé
48589176c7 Doc: Convert README.md to README.rst 2016-01-22 18:20:31 +01:00
Adrien Vergé
38234a1d3c Doc: Generate documentation with Sphinx
HTML documentation should be built with sphinx. This enables easy
integration with Read The Docs [1]. It can also be generated manually by
running:

    make -C docs html

A man page can be generated by running:

    make -C docs man

[1]: http://yamllint.readthedocs.org/
2016-01-22 18:20:28 +01:00
Adrien Vergé
1bfd18097a Rules: indentation: Add 'check-multi-line-strings' option
This options allows the user to control whether to lint indentation
inside multi-line scalars or not.

When enabled, such YAML source will be detected as a problem:

    - C code: void main() {
                  printf("foo");
              }

whereas this one would not:

    - C code: void main() {
              printf("foo");
              }
2016-01-22 14:23:37 +01:00
Adrien Vergé
08f99ccc19 Rules: new-lines: Force type to be in ('unix', 'dos') 2016-01-21 21:59:53 +01:00
Adrien Vergé
7b6f024448 yamllint version 0.4.0 2016-01-20 18:18:35 +01:00
Adrien Vergé
75b4758c95 cli: 'standard' format: Print filename only when error 2016-01-20 17:55:54 +01:00
Adrien Vergé
0e98df2643 cli: Allow passing directories as arguments
For instance:

    yamllint .
    yamllint file.yml ../my-other-dir
2016-01-20 17:55:54 +01:00
Adrien Vergé
d4189083d0 Introduce the 'cli' module and call it from the script 2016-01-20 17:39:26 +01:00
Adrien Vergé
67d13d60ae Rules: indentation: Check multi-line scalars 2016-01-20 17:39:11 +01:00
Adrien Vergé
96465008ab Rules: Fix spaces_before when prev is multi-line scalar
YAML content like the following one produced an error, because the
multi-line ScalarToken ends at the beginning of the 4th line (the one
with the value):

    ? >
        multi-line
        key
    : value
2016-01-20 17:38:48 +01:00
Adrien Vergé
847f7e3fff Rules: comments: Fix bug when multi-line scalar
YAML content like the following one produced an error, because the
ScalarToken associated whose value is "this is plain text" ends at the
beginning of the 5th line (the one with the comment):

    ---
    string: >
      this is plain text

    # comment
2016-01-20 10:45:59 +01:00
Adrien Vergé
6a24781f96 Tests: indentation: Add explicit keys test cases 2016-01-20 10:45:52 +01:00
Adrien Vergé
33224a04e4 yamllint version 0.3.0 2016-01-19 22:58:59 +01:00
Adrien Vergé
fd9d2a00ff Doc: Update README with examples 2016-01-19 22:57:12 +01:00
Adrien Vergé
0b0251bacc Rules: indentation: Add the 'indent-sequences' option
Using either 'yes', 'no' or 'whatever', the user will be able to choose
whether to force block sequence items to be indented, to force them not
to be indented, or don't care, respectively.
2016-01-19 22:37:58 +01:00
Adrien Vergé
ad5cec9c6c Config: Allow overriding only one option when extending 2016-01-19 21:49:58 +01:00
Adrien Vergé
fb14cbdbd9 Config: Allow options to be in a pre-defined list 2016-01-19 21:12:11 +01:00
Adrien Vergé
8288a6f331 Rules: colons: Apply to '?' also 2016-01-19 19:45:13 +01:00
Adrien Vergé
9d8b0d4d2c Rules: commas: Don't allow a comma on a new line
Forbid such constructions:

    [ a, b, c
      , d, e ]
2016-01-19 19:42:56 +01:00
Adrien Vergé
39c878c819 Rules: indentation: Rewrite the algorithm (again)
Use a new, better thought algorithm that keeps an history stack with all
the parents indentations.
2016-01-19 19:42:56 +01:00
Adrien Vergé
222f7a27c1 Make syntax errors prevail over all yamllint problems 2016-01-19 17:18:57 +01:00
Adrien Vergé
effb4db3b4 Tests: Rules: Remove unused line and column args
Now that every test case use the `problem=(x, y)` syntax.
2016-01-19 17:18:57 +01:00
Adrien Vergé
d617eb70ae Rules: Keep a persistent context for token rules
This will be needed to build a clean indentation checking algorithm.
2016-01-19 17:18:57 +01:00
Adrien Vergé
f09aef4f89 Rules: comments-indentation: Allow two levels
Previously only comments that were indented like the following content
line were allowed, e.g.:

    prev: line:
      # commented line
      current: line

With this change, such new cases are also allowed:

      prev: line
      # commented line 1
    # commented line 2
    current: line
2016-01-19 17:18:57 +01:00
Adrien Vergé
01c12f2462 Syntax errors: Use the BaseLoader for safety 2016-01-19 16:35:57 +01:00
Adrien Vergé
918f15b68d Make syntax errors prevail over yamllint 'warnings'
When both a syntax error (unability to parse a document) and a cosmetic
yamllint problem are found at the same place, the yamllint problem had
the priority -- and the syntax error was not displayed.

This had the following problem: if a rule is at the 'warning' level, its
problems will not make the `yamllint` script return a failure return
code (`!= 0`), even when it should (because there was a syntax error,
precisely).

This commit changes this behavior by preferring yamllint problems only
when they have the 'error' level.
2016-01-15 18:46:49 +01:00
Adrien Vergé
97e2210ec9 Don't treat non-importable YAML as syntax error
`yaml.load()` exceptions are not necessarily syntax errors. For
instance, the following YAML source cannot be `load()`ed into a Python
object, but is valid nonetheless:

    ? - Detroit Tigers
      - Chicago cubs
    :
      - 2001-07-23

    ? [ New York Yankees,
        Atlanta Braves ]
    : [ 2001-07-02, 2001-08-12,
        2001-08-14 ]

This commit detects syntax errors from `yaml.parse()` exceptions rather
than `yaml.load_all()`.
2016-01-15 18:46:49 +01:00
Adrien Vergé
1934206cef Rules: comments-indentation: Fix typo 2016-01-15 18:46:40 +01:00
Adrien Vergé
1235eba94e yamllint version 0.2.0 2016-01-15 09:39:50 +01:00
Adrien Vergé
11a14d4df8 Distribution: Update program description 2016-01-14 21:08:10 +01:00
Adrien Vergé
233a70adb3 Rules: Add the 'comments-indentation' rule 2016-01-14 21:04:41 +01:00
Adrien Vergé
e81b73c111 Rules: indentation: Rewrite algorithm 2016-01-14 20:57:35 +01:00
Adrien Vergé
3989a09d32 Rules: comments: Allow empty comments 2016-01-14 19:58:35 +01:00
Adrien Vergé
5cc900f2a8 Rules: document-start: Allow directives 2016-01-14 19:58:05 +01:00
Adrien Vergé
851b9ac42c Rules: Add the 'comments' rule 2016-01-14 11:17:01 +01:00
Adrien Vergé
5c4c208b98 Rules: Add the 'braces' rule 2016-01-14 10:46:16 +01:00
Adrien Vergé
d08eb22081 Rules: Add the 'brackets' rule 2016-01-14 10:46:16 +01:00
Adrien Vergé
a5b384ab21 Rules: Add the 'commas' rule 2016-01-14 10:46:16 +01:00
Adrien Vergé
cfea0661ed Rules: Make max-spaces-* generic
The goal being to use them in the 'colons', 'hyphens', 'commas', etc.
rules.
2016-01-14 10:46:16 +01:00
Adrien Vergé
07c5b4177c Rewrite syntax errors handling and test them
If a syntax errors occurs at the same place than a regular yamllint rule
error, only the yamllint one is issued.
2016-01-14 10:46:16 +01:00
Adrien Vergé
bf96bdde01 Tests: Remove assertIsInstance to support Python 2.6 2016-01-14 10:46:16 +01:00
Adrien Vergé
e2d68dac14 Tests: Travis and Coveralls integration 2016-01-14 10:46:16 +01:00
55 changed files with 3886 additions and 290 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
__pycache__ __pycache__
*.py[cod] *.py[cod]
/docs/_build

18
.travis.yml Normal file
View File

@@ -0,0 +1,18 @@
---
language: python
python:
- 2.6
- 2.7
- 3.3
- 3.4
- 3.5
- nightly
install:
- pip install pyyaml flake8 coveralls
- pip install .
script:
- flake8 .
- yamllint $(git ls-files '*.yml')
- coverage run --source=yamllint setup.py test
after_success:
coveralls

View File

@@ -1,3 +0,0 @@
.PHONY: tests
tests:
python -m unittest discover

View File

@@ -1 +0,0 @@
# yamllint

56
README.rst Normal file
View File

@@ -0,0 +1,56 @@
yamllint
========
A linter for YAML files.
.. image::
https://travis-ci.org/adrienverge/yamllint.svg?branch=master
:target: https://travis-ci.org/adrienverge/yamllint
: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: http://yamllint.readthedocs.org/en/latest/?badge=latest
:alt: Documentation status
Compatible with Python 2 & 3.
Documentation
-------------
http://yamllint.readthedocs.org/
Short overview
--------------
Installation
^^^^^^^^^^^^
.. code:: bash
pip install yamllint
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 custom lint configuration
yamllint -c ~/myconfig file.yml
.. code:: bash
# Output a parsable format (for syntax checking in editors like Vim, emacs...)
yamllint -f parsable file.yml

View File

@@ -15,68 +15,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import os.path
import sys import sys
import argparse from yamllint import cli
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
from yamllint import config
from yamllint.errors import YamlLintConfigError
from yamllint import lint
from yamllint import output
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(prog=APP_NAME, cli.run(sys.argv[1:])
description=APP_DESCRIPTION)
parser.add_argument('files', metavar='FILES', nargs='+',
help='files to check')
parser.add_argument('-c', '--config', dest='config_file', action='store',
help='path to a custom configuration')
parser.add_argument('-f', '--format',
choices=('parsable', 'standard'), default='standard',
help='format for parsing output')
parser.add_argument('-v', '--version', action='version',
version='%s %s' % (APP_NAME, APP_VERSION))
# TODO: read from stdin when no filename?
args = parser.parse_args()
try:
if args.config_file is not None:
conf = config.parse_config_from_file(args.config_file)
elif os.path.isfile('.yamllint'):
conf = config.parse_config_from_file('.yamllint')
else:
conf = config.parse_config('extends: default')
except YamlLintConfigError as e:
print(e, file=sys.stderr)
sys.exit(-1)
return_code = 0
for file in args.files:
if args.format != 'parsable':
print('\033[4m%s\033[0m' % file)
try:
with open(file) as f:
for problem in lint(f, conf):
if args.format == 'parsable':
print(output.parsable_format(problem, file))
else:
print(output.standard_format(problem, file))
if return_code == 0 and problem.level == 'error':
return_code = 1
except EnvironmentError as e:
print(e)
return_code = -1
if args.format != 'parsable':
print('')
sys.exit(return_code)

177
docs/Makefile Normal file
View File

@@ -0,0 +1,177 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
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."

43
docs/conf.py Normal file
View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# yamllint documentation build configuration file, created by
# sphinx-quickstart on Thu Jan 21 21:18:52 2016.
import sys
import os
sys.path.insert(0, os.path.abspath('..')) # noqa
from yamllint import __copyright__, APP_NAME, APP_VERSION
# -- General configuration ------------------------------------------------
extensions = [
'sphinx.ext.autodoc',
]
source_suffix = '.rst'
master_doc = 'index'
project = APP_NAME
copyright = __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', u'yamllint Documentation',
[u'Adrien Vergé'], 1)
]

74
docs/configuration.rst Normal file
View File

@@ -0,0 +1,74 @@
Configuration
=============
yamllint uses a set of *rules* to check sources 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, either name it ``.yamllint`` in your working
directory, or use the ``-c`` option:
::
yamllint -c ~/myconfig file.yml
Default configuration
---------------------
Unless told otherwise, yamllint uses its ``default`` configuration:
.. literalinclude:: ../yamllint/conf/default.yml
:language: yaml
Details on rules can be found on :doc:`the rules page <rules>`.
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 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
Errors and warnings
-------------------
Problems detected by yamllint can be raised either as errors or as warnings.
In both cases, the script will output them (with different colors when using the
``standard`` output format), but the exit code can be different. More precisely,
the script will exit will a failure code *only when* there is one or more
error(s).

11
docs/development.rst Normal file
View File

@@ -0,0 +1,11 @@
Development
===========
yamllint provides both a script and a Python module. The latter can be used to
write your own linting tools:
.. autoclass:: yamllint.errors.LintProblem
:members:
.. automodule:: yamllint
:members:

13
docs/index.rst Normal file
View File

@@ -0,0 +1,13 @@
yamllint documentation
======================
A linter for YAML files.
.. toctree::
:maxdepth: 2
quickstart
configuration
rules
development
text_editors

77
docs/quickstart.rst Normal file
View File

@@ -0,0 +1,77 @@
Quickstart
==========
Installing yamllint
-------------------
First, install yamllint. The easiest way is to use pip, the Python package
manager:
::
sudo pip install yamllint
If you prefer installing from source, you can run, from the source directory:
::
python setup.py sdist
sudo pip install dist/yamllint-*.tar.gz
Running yamllint
----------------
Basic usage:
::
yamllint file.yml other-file.yaml
You can also lint all YAML files in a whole directory:
::
yamllint .
The output will look like (colors are not displayed here [#colored-output]_):
::
file.yml
6:2 warning missing starting space in comment (comments)
57:1 error trailing spaces (trailing-spaces)
60:3 error wrong indentation: expected 4 but found 2 (indentation)
other-file.yml
1:1 warning missing document start "---" (document-start)
9:81 error line too long (84 > 80 characters) (line-length)
31:1 error too many blank lines (4 > 2) (empty-lines)
37:12 error too many spaces inside braces (braces)
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:
::
yamllint -c ~/myconfig file.yml
.. note::
If you have a ``.yamllint`` file in your working directory, it will be
automatically loaded as configuration by yamllint.
.. rubric:: Footnotes
.. [#colored-output] The default output format is colored and inspired by
`eslint <http://eslint.org/>`_, a great linting tool for Javascript.

90
docs/rules.rst Normal file
View File

@@ -0,0 +1,90 @@
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
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
hyphens
-------
.. automodule:: yamllint.rules.hyphens
indentation
-----------
.. automodule:: yamllint.rules.indentation
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
trailing-spaces
---------------
.. automodule:: yamllint.rules.trailing_spaces

40
docs/text_editors.rst Normal file
View File

@@ -0,0 +1,40 @@
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 `syntastic <https://github.com/scrooloose/syntastic>`_ plugin
is installed, add to your ``.vimrc``:
::
TODO
Neovim
------
Assuming that the `neomake <https://github.com/benekastah/neomake>`_ plugin is
installed, add to your ``.config/nvim/init.vim``:
::
if executable('yamllint')
let g:neomake_yaml_yamllint_maker = {
\ 'args': ['-f', 'parsable'],
\ 'errorformat': '%E%f:%l:%c: [error] %m,%W%f:%l:%c: [warning] %m' }
let g:neomake_yaml_enabled_makers = ['yamllint']
endif
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).

View File

@@ -44,7 +44,7 @@ setup(
packages=find_packages(), packages=find_packages(),
scripts=['bin/yamllint'], scripts=['bin/yamllint'],
package_data={'yamllint': ['conf/*.yml']}, package_data={'yamllint': ['conf/*.yml']},
install_requires=[ install_requires=['pyyaml'],
'pyyaml>=3' tests_require=['nose'],
], test_suite='nose.collector',
) )

View File

@@ -33,12 +33,19 @@ class RuleTestCase(unittest.TestCase):
'rules': conf} 'rules': conf}
return parse_config(yaml.safe_dump(conf)) return parse_config(yaml.safe_dump(conf))
def check(self, source, conf, line=None, column=None, **kwargs): def check(self, source, conf, **kwargs):
expected_problems = [] expected_problems = []
for key in kwargs: for key in kwargs:
assert key.startswith('problem') 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( expected_problems.append(
LintProblem(kwargs[key][0], kwargs[key][1], rule=self.rule_id)) LintProblem(kwargs[key][0], kwargs[key][1], rule=rule_id))
expected_problems.sort() expected_problems.sort()
real_problems = list(lint(source, self.build_fake_config(conf))) real_problems = list(lint(source, self.build_fake_config(conf)))

108
tests/rules/test_braces.py Normal file
View File

@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from tests.rules.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_min_spaces(self):
conf = 'braces: {max-spaces-inside: -1, min-spaces-inside: 0}'
self.check('---\n'
'dict: {}\n', conf)
conf = 'braces: {max-spaces-inside: -1, min-spaces-inside: 1}'
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: {max-spaces-inside: -1, min-spaces-inside: 3}'
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: {max-spaces-inside: 0, min-spaces-inside: -1}'
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: {max-spaces-inside: 3, min-spaces-inside: -1}'
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: {max-spaces-inside: 0, min-spaces-inside: 0}'
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: {max-spaces-inside: 1, min-spaces-inside: 1}'
self.check('---\n'
'dict: {a: 1, b, c: 3 }\n', conf, problem=(2, 8))
conf = 'braces: {max-spaces-inside: 2, min-spaces-inside: 0}'
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))

View File

@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from tests.rules.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_min_spaces(self):
conf = 'brackets: {max-spaces-inside: -1, min-spaces-inside: 0}'
self.check('---\n'
'array: []\n', conf)
conf = 'brackets: {max-spaces-inside: -1, min-spaces-inside: 1}'
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: {max-spaces-inside: -1, min-spaces-inside: 3}'
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: {max-spaces-inside: 0, min-spaces-inside: -1}'
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: {max-spaces-inside: 3, min-spaces-inside: -1}'
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: {max-spaces-inside: 0, min-spaces-inside: 0}'
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: {max-spaces-inside: 1, min-spaces-inside: 1}'
self.check('---\n'
'array: [a, b, c ]\n', conf, problem=(2, 9))
conf = 'brackets: {max-spaces-inside: 2, min-spaces-inside: 0}'
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))

View File

@@ -32,16 +32,16 @@ class ColonTestCase(RuleTestCase):
' val\n' ' val\n'
' property : value\n' ' property : value\n'
' prop2 : val2\n' ' prop2 : val2\n'
' propriété : [ valeur ]\n' ' propriété : [valeur]\n'
' o:\n' ' o:\n'
' k1: [v1, v2]\n' ' k1: [v1, v2]\n'
' p:\n' ' p:\n'
' - k3: >\n' ' - k3: >\n'
' val\n' ' val\n'
' - o: { k1: v1 }\n' ' - o: {k1: v1}\n'
' - p: kdjf\n' ' - p: kdjf\n'
' - q: val0\n' ' - q: val0\n'
' q2:\n' ' - q2:\n'
' - val1\n' ' - val1\n'
'...\n', conf) '...\n', conf)
self.check('---\n' self.check('---\n'
@@ -54,7 +54,7 @@ class ColonTestCase(RuleTestCase):
' val\n' ' val\n'
' property: value\n' ' property: value\n'
' prop2: val2\n' ' prop2: val2\n'
' propriété: [ valeur ]\n' ' propriété: [valeur]\n'
' o:\n' ' o:\n'
' k1: [v1, v2]\n', conf) ' k1: [v1, v2]\n', conf)
self.check('---\n' self.check('---\n'
@@ -64,9 +64,9 @@ class ColonTestCase(RuleTestCase):
' val\n' ' val\n'
' - k3: >\n' ' - k3: >\n'
' val\n' ' val\n'
' - o: { k1: v1 }\n' ' - o: {k1: v1}\n'
' - o: { k1: v1 }\n' ' - o: {k1: v1}\n'
' q2:\n' ' - q2:\n'
' - val1\n' ' - val1\n'
'...\n', conf) '...\n', conf)
self.check('---\n' self.check('---\n'
@@ -94,7 +94,7 @@ class ColonTestCase(RuleTestCase):
'...\n', conf, problem=(2, 4)) '...\n', conf, problem=(2, 4))
self.check('---\n' self.check('---\n'
'- lib :\n' '- lib :\n'
' - var\n' ' - var\n'
'...\n', conf, problem=(2, 6)) '...\n', conf, problem=(2, 6))
self.check('---\n' self.check('---\n'
'a: {b: {c : d, e : f}}\n', conf, 'a: {b: {c : d, e : f}}\n', conf,
@@ -118,7 +118,7 @@ class ColonTestCase(RuleTestCase):
'...\n', conf, problem=(3, 8)) '...\n', conf, problem=(3, 8))
def test_before_with_explicit_block_mappings(self): def test_before_with_explicit_block_mappings(self):
conf = 'colons: {max-spaces-before: 0, max-spaces-after: -1}' conf = 'colons: {max-spaces-before: 0, max-spaces-after: 1}'
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' ? key\n' ' ? key\n'
@@ -129,6 +129,30 @@ class ColonTestCase(RuleTestCase):
' ? key\n' ' ? key\n'
' : value\n' ' : value\n'
'...\n', conf, problem=(2, 7)) '...\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): def test_after_enabled(self):
conf = 'colons: {max-spaces-before: -1, max-spaces-after: 1}' conf = 'colons: {max-spaces-before: -1, max-spaces-after: 1}'
@@ -152,6 +176,21 @@ class ColonTestCase(RuleTestCase):
'a: {b: {c: d, e : f}}\n', conf, 'a: {b: {c: d, e : f}}\n', conf,
problem1=(2, 12), problem2=(2, 20)) 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): def test_after_max(self):
conf = 'colons: {max-spaces-before: -1, max-spaces-after: 3}' conf = 'colons: {max-spaces-before: -1, max-spaces-after: 3}'
self.check('---\n' self.check('---\n'

187
tests/rules/test_commas.py Normal file
View File

@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from tests.rules.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)
def test_before_enabled(self):
conf = 'commas: {max-spaces-before: 0, max-spaces-after: -1}'
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(self):
conf = 'commas: {max-spaces-before: 3, max-spaces-after: -1}'
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_enabled(self):
conf = 'commas: {max-spaces-before: -1, max-spaces-after: 1}'
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(self):
conf = 'commas: {max-spaces-before: -1, max-spaces-after: 3}'
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: {max-spaces-before: 0, max-spaces-after: 1}'
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))
def test_comma_on_new_line(self):
conf = 'commas: {max-spaces-before: 0, max-spaces-after: 1}'
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: {max-spaces-before: 0, 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))

View File

@@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from tests.rules.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'
'string: "Une longue phrase." # this is French\n', conf)
def test_starting_space(self):
conf = ('comments:\n'
' require-starting-space: yes\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', 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', conf,
problem1=(2, 2), problem2=(6, 13),
problem4=(9, 2), problem5=(10, 4))
def test_spaces_from_content(self):
conf = ('comments:\n'
' require-starting-space: no\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: yes\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'
'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=(13, 30))
def test_empty_comment(self):
conf = ('comments:\n'
' require-starting-space: yes\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_first_line(self):
conf = ('comments:\n'
' require-starting-space: yes\n'
' min-spaces-from-content: 2\n')
self.check('# comment\n', conf)
def test_multi_line_scalar(self):
conf = ('comments:\n'
' require-starting-space: yes\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)

View File

@@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from tests.rules.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: {}'
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), problem2=(3, 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: {}'
self.check('# comment\n', conf)
self.check(' # comment\n', conf, problem=(1, 3))
def test_no_newline_at_end(self):
conf = ('comments-indentation: {}\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: {}'
self.check('---\n'
'# hey\n'
'# normal\n'
'#\n', conf)
self.check('---\n'
'# hey\n'
'# normal\n'
' #\n', conf, problem=(4, 2))

View File

@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
import yaml
from yamllint.rules.common import (Comment, get_line_indent,
get_comments_between_tokens)
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)
def check_comments(self, buffer, *expected):
yaml_loader = yaml.BaseLoader(buffer)
comments = []
next = yaml_loader.peek_token()
while next is not None:
curr = yaml_loader.get_token()
next = yaml_loader.peek_token()
for comment in get_comments_between_tokens(curr, next):
comments.append(comment)
self.assertEqual(comments, list(expected))
def test_get_comments_between_tokens(self):
self.check_comments('# comment\n',
Comment(1, 1, '# comment', 0))
self.check_comments('---\n'
'# comment\n'
'...\n',
Comment(2, 1, '# comment', 0))
self.check_comments('---\n'
'# no newline char',
Comment(2, 1, '# no newline char', 0))
self.check_comments('# just comment',
Comment(1, 1, '# just comment', 0))
self.check_comments('\n'
' # indented comment\n',
Comment(2, 4, '# indented comment', 0))
self.check_comments('\n'
'# trailing spaces \n',
Comment(2, 1, '# trailing spaces ', 0))
self.check_comments('# comment one\n'
'\n'
'key: val # key=val\n'
'\n'
'# this is\n'
'# a block \n'
'# comment\n'
'\n'
'other:\n'
' - foo # equals\n'
' # bar\n',
Comment(1, 1, '# comment one', 0),
Comment(3, 11, '# key=val', 0),
Comment(5, 1, '# this is', 0),
Comment(6, 1, '# a block ', 0),
Comment(7, 1, '# comment', 0),
Comment(10, 10, '# equals', 0),
Comment(11, 10, '# bar', 0))

View File

@@ -64,12 +64,6 @@ class DocumentEndTestCase(RuleTestCase):
'---\n' '---\n'
'third: document\n' 'third: document\n'
'...\n', conf) '...\n', conf)
self.check('first: document\n'
'...\n'
'second: document\n'
'...\n'
'third: document\n'
'...\n', conf)
self.check('---\n' self.check('---\n'
'first: document\n' 'first: document\n'
'...\n' '...\n'

View File

@@ -82,9 +82,23 @@ class DocumentStartTestCase(RuleTestCase):
'...\n' '...\n'
'second: document\n' 'second: document\n'
'---\n' '---\n'
'third: document\n', conf, problem=(4, 1)) 'third: document\n', conf, problem=(4, 1, 'syntax'))
def test_directives(self): def test_directives(self):
# TODO conf = 'document-start: {present: yes}'
# %YAML 1.2 self.check('%YAML 1.2\n'
pass '---\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)

View File

@@ -36,12 +36,12 @@ class HyphenTestCase(RuleTestCase):
'- elem2\n', conf) '- elem2\n', conf)
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
'- elem1\n' ' - elem1\n'
'- elem2\n', conf) ' - elem2\n', conf)
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
'- elem1\n' ' - elem1\n'
'- elem2\n', conf) ' - elem2\n', conf)
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' subobject:\n' ' subobject:\n'
@@ -69,12 +69,12 @@ class HyphenTestCase(RuleTestCase):
'- elem2\n', conf, problem=(2, 3)) '- elem2\n', conf, problem=(2, 3))
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
'- elem1\n' ' - elem1\n'
'- elem2\n', conf, problem=(4, 3)) ' - elem2\n', conf, problem=(4, 5))
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
'- elem1\n' ' - elem1\n'
'- elem2\n', conf, problem1=(3, 3), problem2=(4, 3)) ' - elem2\n', conf, problem1=(3, 5), problem2=(4, 5))
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' subobject:\n' ' subobject:\n'

View File

@@ -48,37 +48,205 @@ class IndentationTestCase(RuleTestCase):
'...\n', conf) '...\n', conf)
def test_one_space(self): def test_one_space(self):
conf = 'indentation: {spaces: 1}' conf = 'indentation: {spaces: 1, indent-sequences: no}'
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: yes}'
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
' - b\n' ' - b\n'
' k2: v2\n' ' k2: v2\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf) '...\n', conf)
def test_two_spaces(self): def test_two_spaces(self):
conf = 'indentation: {spaces: 2}' conf = 'indentation: {spaces: 2, indent-sequences: no}'
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: 2, indent-sequences: yes}'
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
' - b\n' ' - b\n'
' k2: v2\n' ' k2: v2\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf) '...\n', conf)
def test_three_spaces(self): def test_three_spaces(self):
conf = 'indentation: {spaces: 3}' conf = 'indentation: {spaces: 3, indent-sequences: no}'
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: yes}'
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
' - b\n' ' - b\n'
' k2: v2\n' ' k2: v2\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf) '...\n', conf)
def test_under_indented(self): 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_flow_mappings(self):
conf = 'indentation: {spaces: 2}' conf = 'indentation: {spaces: 2}'
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'
'}\n', conf, problem=(3, 3))
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=(3, 4))
self.check('---\n'
'a: {\n'
' x: 1,\n'
' y, z: 1\n'
' }\n', conf, problem=(5, 3))
def test_flow_sequences(self):
conf = 'indentation: {spaces: 2}'
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'
']\n', conf, problem=(3, 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=(3, 4))
self.check('---\n'
'a: [\n'
' x,\n'
' y, z\n'
' ]\n', conf, problem=(5, 3))
def test_under_indented(self):
conf = 'indentation: {spaces: 2, indent-sequences: yes}'
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' val: 1\n' ' val: 1\n'
@@ -88,7 +256,13 @@ class IndentationTestCase(RuleTestCase):
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
'...\n', conf, problem=(4, 4)) '...\n', conf, problem=(4, 4))
conf = 'indentation: {spaces: 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: yes}'
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' val: 1\n' ' val: 1\n'
@@ -98,9 +272,15 @@ class IndentationTestCase(RuleTestCase):
'- el2:\n' '- el2:\n'
' - subel\n' ' - subel\n'
'...\n', conf, problem=(4, 4)) '...\n', conf, problem=(4, 4))
self.check('---\n'
'object:\n'
' k3:\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf, problem=(5, 10, 'syntax'))
def test_over_indented(self): def test_over_indented(self):
conf = 'indentation: {spaces: 2}' conf = 'indentation: {spaces: 2, indent-sequences: yes}'
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' val: 1\n' ' val: 1\n'
@@ -110,7 +290,13 @@ class IndentationTestCase(RuleTestCase):
' k1:\n' ' k1:\n'
' - a\n' ' - a\n'
'...\n', conf, problem=(4, 6)) '...\n', conf, problem=(4, 6))
conf = 'indentation: {spaces: 4}' 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: yes}'
self.check('---\n' self.check('---\n'
'object:\n' 'object:\n'
' val: 1\n' ' val: 1\n'
@@ -132,42 +318,99 @@ class IndentationTestCase(RuleTestCase):
self.check('---\n' self.check('---\n'
' - el1\n' ' - el1\n'
' - el2:\n' ' - el2:\n'
' - subel\n' ' - subel\n'
'...\n', conf, problem1=(2, 3), problem2=(4, 7)) '...\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))
def test_multi_lines(self): def test_multi_lines(self):
conf = 'indentation: {spaces: 2, indent-sequences: yes}'
self.check('---\n' self.check('---\n'
'long_string: >\n' 'long_string: >\n'
' bla bla blah\n' ' bla bla blah\n'
' blah bla bla\n' ' blah bla bla\n'
'...\n', None) '...\n', conf)
self.check('---\n' self.check('---\n'
'- long_string: >\n' '- long_string: >\n'
' bla bla blah\n' ' bla bla blah\n'
' blah bla bla\n' ' blah bla bla\n'
'...\n', None) '...\n', conf)
self.check('---\n' self.check('---\n'
'obj:\n' 'obj:\n'
' - long_string: >\n' ' - long_string: >\n'
' bla bla blah\n' ' bla bla blah\n'
' blah bla bla\n' ' blah bla bla\n'
'...\n', None) '...\n', conf)
def test_empty_value(self):
conf = 'indentation: {spaces: 2}'
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): def test_nested_collections(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n' self.check('---\n'
'- o:\n' '- o:\n'
' k1: v1\n' ' k1: v1\n'
'...\n', None) '...\n', conf)
self.check('---\n'
'- o:\n'
' k1: v1\n'
'...\n', conf, problem=(3, 2, 'syntax'))
self.check('---\n' self.check('---\n'
'- o:\n' '- o:\n'
' k1: v1\n' ' k1: v1\n'
'...\n', None, problem=(3, 4)) '...\n', conf, problem=(3, 4))
conf = 'indentation: {spaces: 4}'
self.check('---\n'
'- o:\n'
' k1: v1\n'
'...\n', conf)
self.check('---\n' self.check('---\n'
'- o:\n' '- o:\n'
' k1: v1\n' ' k1: v1\n'
'...\n', None, problem=(3, 6)) '...\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_return(self): def test_return(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n' self.check('---\n'
'a:\n' 'a:\n'
' b:\n' ' b:\n'
@@ -176,16 +419,451 @@ class IndentationTestCase(RuleTestCase):
' e:\n' ' e:\n'
' f:\n' ' f:\n'
'g:\n' 'g:\n'
'...\n', None) '...\n', conf)
# self.check('---\n' self.check('---\n'
# 'a:\n' 'a:\n'
# ' b:\n' ' b:\n'
# ' c:\n' ' c:\n'
# ' d:\n' ' d:\n'
# '...\n', None, problem=(5, 5)) '...\n', conf, problem=(5, 4, 'syntax'))
# self.check('---\n' self.check('---\n'
# 'a:\n' 'a:\n'
# ' b:\n' ' b:\n'
# ' c:\n' ' c:\n'
# ' d:\n' ' d:\n'
# '...\n', None, problem=(5, 2)) '...\n', conf, problem=(5, 2, 'syntax'))
def test_first_line(self):
conf = ('indentation: {spaces: 2}\n'
'document-start: disable\n')
self.check(' a: 1\n', conf, problem=(1, 3))
def test_broken_inline_flows(self):
conf = 'indentation: {spaces: 2}'
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))
def test_explicit_block_mappings(self):
conf = 'indentation: {spaces: 4}'
self.check('---\n'
'object:\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'
'object:\n'
' ?\n'
' key\n'
' :\n'
' value\n'
'...\n', conf)
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))
class ScalarIndentationTestCase(RuleTestCase):
rule_id = 'indentation'
def test_basics_plain(self):
conf = ('indentation: {spaces: 2, check-multi-line-strings: no}\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, problem=(2, 3))
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: 2, check-multi-line-strings: yes}\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, 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: 2, check-multi-line-strings: no}\n'
'document-start: disable\n')
self.check('"multi\n'
' line"\n', conf)
self.check('"multi\n'
'line"\n', conf, problem=(2, 1))
self.check('- "multi\n'
' line"\n', conf)
self.check('- "multi\n'
' line"\n', conf, problem=(2, 3))
self.check('a key: "multi\n'
' line"\n', conf)
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:\n'
' "multi\n'
' line"\n', conf)
self.check('a key:\n'
' "multi\n'
' line"\n', conf, problem=(3, 3))
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)
def test_check_multi_line_quoted(self):
conf = ('indentation: {spaces: 2, check-multi-line-strings: yes}\n'
'document-start: disable\n')
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, 10))
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))
def test_basics_folded_style(self):
conf = ('indentation: {spaces: 2, check-multi-line-strings: no}\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: 2, check-multi-line-strings: yes}\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: 2, check-multi-line-strings: no}\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: 2, check-multi-line-strings: yes}\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: 2, check-multi-line-strings: yes}\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: 2, check-multi-line-strings: yes}\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: 2, check-multi-line-strings: yes}\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: 2, check-multi-line-strings: yes}\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: 2, check-multi-line-strings: yes}\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))

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from tests.rules.common import RuleTestCase
class YamlLintTestCase(RuleTestCase):
rule_id = None # syntax error
def test_lint(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))
def test_directives(self):
self.check('%YAML 1.2\n'
'%TAG ! tag:clarkevans.com,2002:\n'
'doc: ument\n'
'...\n', None, problem=(3, 1))
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)

View File

@@ -33,11 +33,11 @@ class TrailingSpacesTestCase(RuleTestCase):
self.check('', conf) self.check('', conf)
self.check('\n', conf) self.check('\n', conf)
self.check(' \n', conf, problem=(1, 1)) self.check(' \n', conf, problem=(1, 1))
self.check('\t\t\t\n', conf, problem=(1, 1)) self.check('\t\t\t\n', conf, problem=(1, 1, 'syntax'))
self.check('---\n' self.check('---\n'
'some: text \n', conf, problem=(2, 11)) 'some: text \n', conf, problem=(2, 11))
self.check('---\n' self.check('---\n'
'some: text\t\n', conf, problem=(2, 11)) 'some: text\t\n', conf, problem=(2, 11, 'syntax'))
def test_with_dos_new_lines(self): def test_with_dos_new_lines(self):
conf = ('trailing-spaces: {}\n' conf = ('trailing-spaces: {}\n'

69
tests/test_config.py Normal file
View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import unittest
from yamllint import config
class ConfigTestCase(unittest.TestCase):
def setUp(self):
self.base = config.parse_config_from_file(os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
'yamllint', 'conf', 'default.yml'))
def test_extend_config_disable_rule(self):
new = config.parse_config('extends: default\n'
'rules:\n'
' trailing-spaces: disable\n')
base = self.base.copy()
del base['trailing-spaces']
self.assertEqual(sorted(new.keys()), sorted(base.keys()))
for rule in new:
self.assertEqual(new[rule], base[rule])
def test_extend_config_override_whole_rule(self):
new = config.parse_config('extends: default\n'
'rules:\n'
' empty-lines:\n'
' max: 42\n'
' max-start: 43\n'
' max-end: 44\n')
base = self.base.copy()
base['empty-lines']['max'] = 42
base['empty-lines']['max-start'] = 43
base['empty-lines']['max-end'] = 44
self.assertEqual(sorted(new.keys()), sorted(base.keys()))
for rule in new:
self.assertEqual(new[rule], base[rule])
def test_extend_config_override_rule_partly(self):
new = config.parse_config('extends: default\n'
'rules:\n'
' empty-lines:\n'
' max-start: 42\n')
base = self.base.copy()
base['empty-lines']['max-start'] = 42
self.assertEqual(sorted(new.keys()), sorted(base.keys()))
for rule in new:
self.assertEqual(new[rule], base[rule])

View File

@@ -65,8 +65,8 @@ class ParserTestCase(unittest.TestCase):
e = list(token_generator('')) e = list(token_generator(''))
self.assertEqual(len(e), 2) self.assertEqual(len(e), 2)
self.assertEqual(e[0].prev, None) self.assertEqual(e[0].prev, None)
self.assertIsInstance(e[0].curr, yaml.Token) self.assertTrue(isinstance(e[0].curr, yaml.Token))
self.assertIsInstance(e[0].next, yaml.Token) self.assertTrue(isinstance(e[0].next, yaml.Token))
self.assertEqual(e[1].prev, e[0].curr) self.assertEqual(e[1].prev, e[0].curr)
self.assertEqual(e[1].curr, e[0].next) self.assertEqual(e[1].curr, e[0].next)
self.assertEqual(e[1].next, None) self.assertEqual(e[1].next, None)
@@ -74,20 +74,20 @@ class ParserTestCase(unittest.TestCase):
e = list(token_generator('---\n' e = list(token_generator('---\n'
'k: v\n')) 'k: v\n'))
self.assertEqual(len(e), 9) self.assertEqual(len(e), 9)
self.assertIsInstance(e[3].curr, yaml.KeyToken) self.assertTrue(isinstance(e[3].curr, yaml.KeyToken))
self.assertIsInstance(e[5].curr, yaml.ValueToken) self.assertTrue(isinstance(e[5].curr, yaml.ValueToken))
def test_token_or_line_generator(self): def test_token_or_line_generator(self):
e = list(token_or_line_generator('---\n' e = list(token_or_line_generator('---\n'
'k: v\n')) 'k: v\n'))
self.assertEqual(len(e), 12) self.assertEqual(len(e), 12)
self.assertIsInstance(e[0], Token) self.assertTrue(isinstance(e[0], Token))
self.assertIsInstance(e[0].curr, yaml.StreamStartToken) self.assertTrue(isinstance(e[0].curr, yaml.StreamStartToken))
self.assertIsInstance(e[1], Token) self.assertTrue(isinstance(e[1], Token))
self.assertIsInstance(e[1].curr, yaml.DocumentStartToken) self.assertTrue(isinstance(e[1].curr, yaml.DocumentStartToken))
self.assertIsInstance(e[2], Line) self.assertTrue(isinstance(e[2], Line))
self.assertIsInstance(e[3].curr, yaml.BlockMappingStartToken) self.assertTrue(isinstance(e[3].curr, yaml.BlockMappingStartToken))
self.assertIsInstance(e[4].curr, yaml.KeyToken) self.assertTrue(isinstance(e[4].curr, yaml.KeyToken))
self.assertIsInstance(e[6].curr, yaml.ValueToken) self.assertTrue(isinstance(e[6].curr, yaml.ValueToken))
self.assertIsInstance(e[8], Line) self.assertTrue(isinstance(e[8], Line))
self.assertIsInstance(e[11], Line) self.assertTrue(isinstance(e[11], Line))

View File

@@ -14,42 +14,41 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import yaml
from yamllint import config from yamllint import config
from yamllint.errors import LintProblem
from yamllint import parser from yamllint import parser
APP_NAME = 'yamllint' APP_NAME = 'yamllint'
APP_VERSION = '0.1.0' APP_VERSION = '0.5.0'
APP_DESCRIPTION = 'Lint YAML files.' APP_DESCRIPTION = 'A linter for YAML files.'
__author__ = 'Adrien Vergé' __author__ = u'Adrien Vergé'
__copyright__ = 'Copyright 2016, Adrien Vergé' __copyright__ = u'Copyright 2016, Adrien Vergé'
__license__ = 'GPLv3' __license__ = 'GPLv3'
__version__ = APP_VERSION __version__ = APP_VERSION
def _lint(buffer, conf): def get_costemic_problems(buffer, conf):
rules = config.get_enabled_rules(conf) rules = config.get_enabled_rules(conf)
# Split token rules from line rules # Split token rules from line rules
token_rules = [r for r in rules if r.TYPE == 'token'] token_rules = [r for r in rules if r.TYPE == 'token']
line_rules = [r for r in rules if r.TYPE == 'line'] line_rules = [r for r in rules if r.TYPE == 'line']
# If the document contains a syntax error, save it and yield it at the context = {}
# right line for rule in token_rules:
syntax_error = parser.get_syntax_error(buffer) context[rule.ID] = {}
for elem in parser.token_or_line_generator(buffer): for elem in parser.token_or_line_generator(buffer):
if syntax_error and syntax_error.line <= elem.line_no:
syntax_error.level = 'error'
yield syntax_error
syntax_error = None
if isinstance(elem, parser.Token): if isinstance(elem, parser.Token):
for rule in token_rules: for rule in token_rules:
rule_conf = conf[rule.ID] rule_conf = conf[rule.ID]
for problem in rule.check(rule_conf, for problem in rule.check(rule_conf,
elem.curr, elem.prev, elem.next): elem.curr, elem.prev, elem.next,
context[rule.ID]):
problem.rule = rule.ID problem.rule = rule.ID
problem.level = rule_conf['level'] problem.level = rule_conf['level']
yield problem yield problem
@@ -62,6 +61,44 @@ def _lint(buffer, conf):
yield problem yield problem
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)
problem.level = 'error'
return problem
def _lint(buffer, conf):
# 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_costemic_problems(buffer, conf):
# 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
# If there is already a yamllint error at the same place, discard
# it as it is probably redundant (and maybe it's just a 'warning',
# in which case the script won't even exit with a failure status).
if (syntax_error.line == problem.line and
syntax_error.column == problem.column):
syntax_error = None
continue
syntax_error = None
yield problem
if syntax_error:
yield syntax_error
def lint(input, conf): def lint(input, conf):
"""Lints a YAML source. """Lints a YAML source.

119
yamllint/cli.py Normal file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import os.path
import sys
import argparse
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
from yamllint import config
from yamllint.errors import YamlLintConfigError
from yamllint import lint
def find_files_recursively(items):
for item in items:
if os.path.isdir(item):
for root, dirnames, filenames in os.walk(item):
for filename in [f for f in filenames
if f.endswith(('.yml', '.yaml'))]:
yield os.path.join(root, filename)
else:
yield item
class Format(object):
@staticmethod
def parsable(problem, filename):
return ('%(file)s:%(line)s:%(column)s: [%(level)s] %(message)s' %
{'file': filename,
'line': problem.line,
'column': problem.column,
'level': problem.level,
'message': problem.message})
@staticmethod
def standard(problem, filename):
line = ' \033[2m%d:%d\033[0m' % (problem.line, problem.column)
line += max(20 - len(line), 0) * ' '
if problem.level == 'warning':
line += '\033[33m%s\033[0m' % problem.level
else:
line += '\033[31m%s\033[0m' % problem.level
line += max(38 - len(line), 0) * ' '
line += problem.desc
if problem.rule:
line += ' \033[2m(%s)\033[0m' % problem.rule
return line
def run(argv):
parser = argparse.ArgumentParser(prog=APP_NAME,
description=APP_DESCRIPTION)
parser.add_argument('files', metavar='FILE_OR_DIR', nargs='+',
help='files to check')
parser.add_argument('-c', '--config', dest='config_file', action='store',
help='path to a custom configuration')
parser.add_argument('-f', '--format',
choices=('parsable', 'standard'), default='standard',
help='format for parsing output')
parser.add_argument('-v', '--version', action='version',
version='%s %s' % (APP_NAME, APP_VERSION))
# TODO: read from stdin when no filename?
args = parser.parse_args(argv)
try:
if args.config_file is not None:
conf = config.parse_config_from_file(args.config_file)
elif os.path.isfile('.yamllint'):
conf = config.parse_config_from_file('.yamllint')
else:
conf = config.parse_config('extends: default')
except YamlLintConfigError as e:
print(e, file=sys.stderr)
sys.exit(-1)
return_code = 0
for file in find_files_recursively(args.files):
try:
first = True
with open(file) as f:
for problem in lint(f, conf):
if args.format == 'parsable':
print(Format.parsable(problem, file))
else:
if first:
print('\033[4m%s\033[0m' % file)
first = False
print(Format.standard(problem, file))
if return_code == 0 and problem.level == 'error':
return_code = 1
if not first and args.format != 'parsable':
print('')
except EnvironmentError as e:
print(e)
return_code = -1
sys.exit(return_code)

View File

@@ -1,17 +1,24 @@
--- ---
rules: rules:
#block-sequence-indentation: braces:
# present: yes min-spaces-inside: 0
max-spaces-inside: 0
brackets:
min-spaces-inside: 0
max-spaces-inside: 0
colons: colons:
max-spaces-before: 0 max-spaces-before: 0
max-spaces-after: 1 max-spaces-after: 1
#comma: commas:
# max-spaces-before: 0 max-spaces-before: 0
# max-spaces-after: 1 max-spaces-after: 1
#comment: comments:
# min-spaces-after: 1 level: warning
# min-spaces-before: 2 require-starting-space: yes
min-spaces-from-content: 2
comments-indentation:
level: warning
document-end: disable document-end: disable
document-start: document-start:
level: warning level: warning
@@ -24,16 +31,11 @@ rules:
max-spaces-after: 1 max-spaces-after: 1
indentation: indentation:
spaces: 2 spaces: 2
#comments: yes indent-sequences: yes
check-multi-line-strings: no
line-length: line-length:
max: 80 max: 80
new-line-at-end-of-file: {level: error} new-line-at-end-of-file: {level: error}
new-lines: new-lines:
type: unix type: unix
#spaces-in-brackets: [ 1, 2 ]
# min: 1
# max: 1
#spaces-in-braces: { df: d }
# min: 1
# max: 1
trailing-spaces: {} trailing-spaces: {}

View File

@@ -46,7 +46,11 @@ def extend_config(content):
if 'extends' in conf: if 'extends' in conf:
base = parse_config_from_file(get_extended_conf(conf['extends'])) base = parse_config_from_file(get_extended_conf(conf['extends']))
base.update(conf['rules']) for rule in conf['rules']:
if type(conf['rules'][rule]) == dict and rule in base:
base[rule].update(conf['rules'][rule])
else:
base[rule] = conf['rules'][rule]
conf['rules'] = base conf['rules'] = base
return conf return conf
@@ -83,10 +87,16 @@ def parse_config(content):
raise YamlLintConfigError( raise YamlLintConfigError(
'invalid config: unknown option "%s" for rule "%s"' % 'invalid config: unknown option "%s" for rule "%s"' %
(optkey, id)) (optkey, id))
if type(conf['rules'][id][optkey]) != options[optkey]: if type(options[optkey]) == tuple:
raise YamlLintConfigError( if conf['rules'][id][optkey] not in options[optkey]:
'invalid config: option "%s" of "%s" should be %s' % raise YamlLintConfigError(
(optkey, id, options[optkey].__name__)) ('invalid config: option "%s" of "%s" should be '
'in %s') % (optkey, id, options[optkey]))
else:
if type(conf['rules'][id][optkey]) != options[optkey]:
raise YamlLintConfigError(
('invalid config: option "%s" of "%s" should be '
'%s' % (optkey, id, options[optkey].__name__)))
rules[id][optkey] = conf['rules'][id][optkey] rules[id][optkey] = conf['rules'][id][optkey]
else: else:
raise YamlLintConfigError(('invalid config: rule "%s": should be ' raise YamlLintConfigError(('invalid config: rule "%s": should be '

View File

@@ -16,10 +16,15 @@
class LintProblem(object): class LintProblem(object):
"""Represents a linting problem found by yamllint."""
def __init__(self, line, column, desc='<no description>', rule=None): def __init__(self, line, column, desc='<no description>', rule=None):
#: Line on which the problem was found (starting at 1)
self.line = line self.line = line
#: Column on which the problem was found (starting at 1)
self.column = column self.column = column
#: Human-readable description of the problem
self.desc = desc self.desc = desc
#: Identifier of the rule that detected the problem
self.rule = rule self.rule = rule
self.level = None self.level = None

View File

@@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
def parsable_format(problem, filename):
return ('%(file)s:%(line)s:%(column)s: [%(level)s] %(message)s' %
{'file': filename,
'line': problem.line,
'column': problem.column,
'level': problem.level,
'message': problem.message})
def standard_format(problem, filename):
line = ' \033[2m%d:%d\033[0m' % (problem.line, problem.column)
line += max(20 - len(line), 0) * ' '
if problem.level == 'warning':
line += '\033[33m%s\033[0m' % problem.level
else:
line += '\033[31m%s\033[0m' % problem.level
line += max(38 - len(line), 0) * ' '
line += problem.desc
if problem.rule:
line += ' \033[2m(%s)\033[0m' % problem.rule
return line

View File

@@ -16,8 +16,6 @@
import yaml import yaml
from yamllint.errors import LintProblem
class Line(object): class Line(object):
def __init__(self, line_no, buffer, start, end): def __init__(self, line_no, buffer, start, end):
@@ -85,11 +83,3 @@ def token_or_line_generator(buffer):
else: else:
yield token yield token
token = next(token_gen, None) token = next(token_gen, None)
def get_syntax_error(buffer):
try:
yaml.safe_load_all(buffer)
except yaml.error.MarkedYAMLError as e:
return LintProblem(e.problem_mark.line + 1, e.problem_mark.column + 1,
'syntax error: ' + e.problem)

View File

@@ -15,7 +15,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from yamllint.rules import ( from yamllint.rules import (
braces,
brackets,
colons, colons,
commas,
comments,
comments_indentation,
document_end, document_end,
document_start, document_start,
empty_lines, empty_lines,
@@ -28,7 +33,12 @@ from yamllint.rules import (
) )
_RULES = { _RULES = {
braces.ID: braces,
brackets.ID: brackets,
colons.ID: colons, colons.ID: colons,
commas.ID: commas,
comments.ID: comments,
comments_indentation.ID: comments_indentation,
document_end.ID: document_end, document_end.ID: document_end,
document_start.ID: document_start, document_start.ID: document_start,
empty_lines.ID: empty_lines, empty_lines.ID: empty_lines,

95
yamllint/rules/braces.py Normal file
View File

@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Use this rule to control the number of spaces inside braces (``{`` and ``}``).
.. rubric:: Options
* ``min-spaces-inside`` defines the minimal number of spaces required inside
braces.
* ``max-spaces-inside`` defines the maximal number of spaces allowed inside
braces.
.. rubric:: Examples
#. 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 }
"""
import yaml
from yamllint.rules.common import spaces_after, spaces_before
ID = 'braces'
TYPE = 'token'
CONF = {'min-spaces-inside': int,
'max-spaces-inside': int}
def check(conf, token, prev, next, context):
if 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

View File

@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Use this rule to control the number of spaces inside brackets (``[`` and
``]``).
.. rubric:: Options
* ``min-spaces-inside`` defines the minimal number of spaces required inside
brackets.
* ``max-spaces-inside`` defines the maximal number of spaces allowed inside
brackets.
.. rubric:: Examples
#. 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 ]
"""
import yaml
from yamllint.rules.common import spaces_after, spaces_before
ID = 'brackets'
TYPE = 'token'
CONF = {'min-spaces-inside': int,
'max-spaces-inside': int}
def check(conf, token, prev, next, context):
if 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

View File

@@ -14,9 +14,65 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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:: 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 import yaml
from yamllint.errors import LintProblem from yamllint.rules.common import spaces_after, spaces_before, is_explicit_key
ID = 'colons' ID = 'colons'
@@ -25,22 +81,23 @@ CONF = {'max-spaces-before': int,
'max-spaces-after': int} 'max-spaces-after': int}
def check(conf, token, prev, next): def check(conf, token, prev, next, context):
if isinstance(token, yaml.ValueToken): if isinstance(token, yaml.ValueToken):
if (prev is not None and problem = spaces_before(token, prev, next,
prev.end_mark.line == token.start_mark.line and max=conf['max-spaces-before'],
conf['max-spaces-before'] != - 1 and max_desc='too many spaces before colon')
(prev.end_mark.pointer + conf['max-spaces-before'] < if problem is not None:
token.start_mark.pointer)): yield problem
yield LintProblem(token.start_mark.line + 1,
token.start_mark.column,
'too many spaces before colon')
if (next is not None and problem = spaces_after(token, prev, next,
token.end_mark.line == next.start_mark.line and max=conf['max-spaces-after'],
conf['max-spaces-after'] != - 1 and max_desc='too many spaces after colon')
(next.start_mark.pointer - token.end_mark.pointer > if problem is not None:
conf['max-spaces-after'])): yield problem
yield LintProblem(token.start_mark.line + 1,
next.start_mark.column, if isinstance(token, yaml.KeyToken) and is_explicit_key(token):
'too many spaces after colon') 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

94
yamllint/rules/commas.py Normal file
View File

@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <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).
* ``max-spaces-after`` defines the maximal number of spaces allowed after
commas (use ``-1`` to disable).
.. rubric:: Examples
#. With ``commas: {max-spaces-before: 0, 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}]
the following code snippet would **FAIL**:
::
strange var:
[10, 20, 30, {x: 1, y: 2}]
#. With ``commas: {max-spaces-before: 2, max-spaces-after: 2}``
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}]
"""
import yaml
from yamllint.errors import LintProblem
from yamllint.rules.common import spaces_after, spaces_before
ID = 'commas'
TYPE = 'token'
CONF = {'max-spaces-before': int,
'max-spaces-after': int}
def check(conf, token, prev, next, context):
if isinstance(token, yaml.FlowEntryToken):
if prev is not None 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,
max=conf['max-spaces-after'],
max_desc='too many spaces after comma')
if problem is not None:
yield problem

View File

@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <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 ``yes`` to enable, ``no`` to disable.
* ``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:: Examples
#. With ``comments: {require-starting-space: yes}``
the following code snippet would **PASS**:
::
# This sentence
# is a block comment
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
"""
import yaml
from yamllint.errors import LintProblem
from yamllint.rules.common import get_comments_between_tokens
ID = 'comments'
TYPE = 'token'
CONF = {'require-starting-space': bool,
'min-spaces-from-content': int}
def check(conf, token, prev, next, context):
for comment in get_comments_between_tokens(token, next):
if (conf['min-spaces-from-content'] != -1 and
not isinstance(token, yaml.StreamStartToken) and
comment.line == token.end_mark.line + 1):
# Sometimes token end marks are on the next line
if token.end_mark.buffer[token.end_mark.pointer - 1] != '\n':
if (comment.pointer - token.end_mark.pointer <
conf['min-spaces-from-content']):
yield LintProblem(comment.line, comment.column,
'too few spaces before comment')
if (conf['require-starting-space'] and
comment.pointer + 1 < len(comment.buffer) and
comment.buffer[comment.pointer + 1] != ' ' and
comment.buffer[comment.pointer + 1] != '\n'):
yield LintProblem(comment.line, comment.column + 1,
'missing starting space in comment')

View File

@@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <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.errors import LintProblem
from yamllint.rules.common import get_line_indent, get_comments_between_tokens
ID = 'comments-indentation'
TYPE = 'token'
# Case A:
#
# prev: line:
# # commented line
# current: line
#
# Case B:
#
# prev: line
# # commented line 1
# # commented line 2
# current: line
def check(conf, token, prev, next, context):
if prev is None:
return
curr_line_indent = token.start_mark.column
if isinstance(token, yaml.StreamEndToken):
curr_line_indent = 0
skip_first_line = True
if isinstance(prev, yaml.StreamStartToken):
skip_first_line = False
prev_line_indent = 0
else:
prev_line_indent = get_line_indent(prev)
if prev_line_indent <= curr_line_indent:
prev_line_indent = -1 # disable it
for comment in get_comments_between_tokens(
prev, token, skip_first_line=skip_first_line):
if comment.column - 1 == curr_line_indent:
prev_line_indent = -1 # disable it
elif comment.column - 1 != prev_line_indent:
yield LintProblem(comment.line, comment.column,
'comment not indented like content')

118
yamllint/rules/common.py Normal file
View File

@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import yaml
from yamllint.errors 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)
class Comment(object):
def __init__(self, line, column, buffer, pointer):
self.line = line
self.column = column
self.buffer = buffer
self.pointer = pointer
def __repr__(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 (self.line == other.line and
self.column == other.column and
str(self) == str(other))
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_comments_between_tokens(token1, token2, skip_first_line=False):
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
for line in buf.split('\n'):
if skip_first_line:
skip_first_line = False
else:
pos = line.find('#')
if pos != -1:
yield Comment(line_no, column_no + pos,
token1.end_mark.buffer, pointer + pos)
pointer += len(line) + 1
line_no += 1
column_no = 1
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] == '?')

View File

@@ -14,6 +14,66 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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 ``yes`` when the document end marker is required, or to
``no`` when it is forbidden.
.. rubric:: Examples
#. With ``document-end: {present: yes}``
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: no}``
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 import yaml
from yamllint.errors import LintProblem from yamllint.errors import LintProblem
@@ -24,7 +84,7 @@ TYPE = 'token'
CONF = {'present': bool} CONF = {'present': bool}
def check(conf, token, prev, next): def check(conf, token, prev, next, context):
if conf['present']: if conf['present']:
if (isinstance(token, yaml.StreamEndToken) and if (isinstance(token, yaml.StreamEndToken) and
not (isinstance(prev, yaml.DocumentEndToken) or not (isinstance(prev, yaml.DocumentEndToken) or

View File

@@ -14,6 +14,56 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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 ``yes`` when the document start marker is required, or to
``no`` when it is forbidden.
.. rubric:: Examples
#. With ``document-start: {present: yes}``
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: no}``
the following code snippet would **PASS**:
::
this:
is: [a, document]
...
the following code snippet would **FAIL**:
::
---
this:
is: [a, document]
...
"""
import yaml import yaml
from yamllint.errors import LintProblem from yamllint.errors import LintProblem
@@ -24,15 +74,14 @@ TYPE = 'token'
CONF = {'present': bool} CONF = {'present': bool}
# TODO: Don't fail if document contains directives such as def check(conf, token, prev, next, context):
# %YAML 1.2
def check(conf, token, prev, next):
if conf['present']: if conf['present']:
if ((isinstance(prev, yaml.StreamStartToken) or if (isinstance(prev, (yaml.StreamStartToken,
isinstance(prev, yaml.DocumentEndToken)) and yaml.DocumentEndToken,
not (isinstance(token, yaml.DocumentStartToken) or yaml.DirectiveToken)) and
isinstance(token, yaml.StreamEndToken))): not isinstance(token, (yaml.DocumentStartToken,
yaml.DirectiveToken,
yaml.StreamEndToken))):
yield LintProblem(token.start_mark.line + 1, 1, yield LintProblem(token.start_mark.line + 1, 1,
'missing document start "---"') 'missing document start "---"')

View File

@@ -14,6 +14,42 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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:: 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.errors import LintProblem from yamllint.errors import LintProblem

View File

@@ -14,9 +14,63 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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:: 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 import yaml
from yamllint.errors import LintProblem from yamllint.rules.common import spaces_after
ID = 'hyphens' ID = 'hyphens'
@@ -24,11 +78,10 @@ TYPE = 'token'
CONF = {'max-spaces-after': int} CONF = {'max-spaces-after': int}
def check(conf, token, prev, next): def check(conf, token, prev, next, context):
if isinstance(token, yaml.BlockEntryToken): if isinstance(token, yaml.BlockEntryToken):
if token.end_mark.line == next.start_mark.line: problem = spaces_after(token, prev, next,
if (next.start_mark.pointer - token.end_mark.pointer > max=conf['max-spaces-after'],
conf['max-spaces-after']): max_desc='too many spaces after hyphen')
yield LintProblem(token.start_mark.line + 1, if problem is not None:
next.start_mark.column, yield problem
'too many spaces after hyphen')

View File

@@ -14,50 +14,371 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Use this rule to control the indentation.
.. rubric:: Options
* ``spaces`` defines the number of spaces that represent an indentation level.
* ``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: ``yes``,
``no`` and ``whatever`` (the latter means either indenting or not indenting
block sequences is OK.
* ``check-multi-line-strings`` defines whether to lint indentation in
multi-line strings. Set to ``yes`` to enable, ``no`` to disable.
.. 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: 2, indent-sequences: no}``
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: 4, check-multi-line-strings: yes}``
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**:
::
C code:
void main() {
printf("foo");
}
the following code snippet would **PASS**:
::
C code:
void main() {
printf("bar");
}
"""
import yaml import yaml
from yamllint.errors import LintProblem from yamllint.errors import LintProblem
from yamllint.rules.common import is_explicit_key
ID = 'indentation' ID = 'indentation'
TYPE = 'token' TYPE = 'token'
CONF = {'spaces': int} CONF = {'spaces': int,
'indent-sequences': (True, False, 'whatever'),
'check-multi-line-strings': bool}
ROOT, MAP, B_SEQ, F_SEQ, KEY, VAL = range(6)
def check(conf, token, prev, next): class Parent(object):
if isinstance(token, yaml.StreamEndToken): def __init__(self, type, indent):
self.type = type
self.indent = indent
self.explicit_key = False
def check_scalar_indentation(conf, token, context):
if token.start_mark.line == token.end_mark.line:
return return
if (prev is None or isinstance(prev, yaml.StreamStartToken) or if token.plain:
isinstance(prev, yaml.DirectiveToken) or expected_indent = token.start_mark.column
isinstance(prev, yaml.DocumentStartToken)): elif token.style in ('"', "'"):
if token.start_mark.column != 0: expected_indent = token.start_mark.column + 1
yield LintProblem( elif token.style in ('>', '|'):
token.end_mark.line + 1, token.start_mark.column + 1, if context['stack'][-1].type == B_SEQ:
'found indentation of %d instead of %d' % # - >
(token.start_mark.column, 0)) # multi
return # line
expected_indent = token.start_mark.column + conf['spaces']
elif context['stack'][-1].type == KEY:
assert context['stack'][-1].explicit_key
# - ? >
# multi-line
# key
# : >
# multi-line
# value
expected_indent = token.start_mark.column + conf['spaces']
elif context['stack'][-1].type == VAL:
if token.start_mark.line + 1 > context['cur_line']:
# - key:
# >
# multi
# line
expected_indent = context['stack'][-1].indent + conf['spaces']
elif context['stack'][-2].explicit_key:
# - ? key
# : >
# multi-line
# value
expected_indent = token.start_mark.column + conf['spaces']
else:
# - key: >
# multi
# line
expected_indent = context['stack'][-2].indent + conf['spaces']
else:
expected_indent = context['stack'][-1].indent + conf['spaces']
if token.start_mark.line > prev.end_mark.line: line_no = token.start_mark.line + 1
buffer = prev.end_mark.buffer
start = buffer.rfind('\n', 0, prev.end_mark.pointer) + 1 line_start = token.start_mark.pointer
prev_indent = 0 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
# YAML recognizes two white space characters: space and tab. indent = 0
# http://yaml.org/spec/1.2/spec.html#id2775170 while token.start_mark.buffer[line_start + indent] == ' ':
while buffer[start + prev_indent] in ' \t': indent += 1
prev_indent += 1 if token.start_mark.buffer[line_start + indent] == '\n':
continue
# Discard any leading '- ' if indent < expected_indent:
if (buffer[start + prev_indent:start + prev_indent + 2] == '- '): yield LintProblem(line_no, indent + 1,
prev_indent += 2 ('wrong indentation: expected at least %d but '
while buffer[start + prev_indent] in ' \t': 'found %d') % (expected_indent, indent))
prev_indent += 1 elif conf['check-multi-line-strings'] and indent > expected_indent:
yield LintProblem(line_no, indent + 1,
'wrong indentation: expected %d but found %d' %
(expected_indent, indent))
if (token.start_mark.column > prev_indent and
token.start_mark.column != prev_indent + conf['spaces']): def check(conf, token, prev, next, context):
yield LintProblem( if 'stack' not in context:
token.end_mark.line + 1, token.start_mark.column + 1, context['stack'] = [Parent(ROOT, 0)]
'found indentation of %d instead of %d' % context['cur_line'] = -1
(token.start_mark.column, prev_indent + conf['spaces']))
# Step 1: Lint
needs_lint = (
not isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)) and
not isinstance(token, yaml.BlockEndToken) and
not (isinstance(token, yaml.ScalarToken) and token.value == '') and
token.start_mark.line + 1 > context['cur_line'])
if needs_lint:
found_indentation = token.start_mark.column
expected = context['stack'][-1].indent
if isinstance(token, (yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken)):
expected = 0
elif (context['stack'][-1].type == KEY and
context['stack'][-1].explicit_key and
not isinstance(token, yaml.ValueToken)):
expected += conf['spaces']
if found_indentation != expected:
yield LintProblem(token.start_mark.line + 1, found_indentation + 1,
'wrong indentation: expected %d but found %d' %
(expected, found_indentation))
if isinstance(token, yaml.ScalarToken):
for problem in check_scalar_indentation(conf, token, context):
yield problem
# Step 2.a:
if needs_lint:
context['cur_line_indent'] = found_indentation
context['cur_line'] = token.end_mark.line + 1
# Step 2.b: Update state
if isinstance(token, yaml.BlockMappingStartToken):
assert isinstance(next, yaml.KeyToken)
if next.start_mark.line == token.start_mark.line:
# - a: 1
# b: 2
# or
# - ? a
# : 1
indent = token.start_mark.column
else:
# - ?
# a
# : 1
indent = token.start_mark.column + conf['spaces']
context['stack'].append(Parent(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 = context['cur_line_indent'] + conf['spaces']
context['stack'].append(Parent(MAP, indent))
elif isinstance(token, yaml.BlockSequenceStartToken):
# - - a
# - b
assert next.start_mark.line == token.start_mark.line
assert isinstance(next, yaml.BlockEntryToken)
indent = token.start_mark.column
context['stack'].append(Parent(B_SEQ, 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 = context['cur_line_indent'] + conf['spaces']
context['stack'].append(Parent(F_SEQ, indent))
elif isinstance(token, (yaml.BlockEndToken,
yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken)):
assert context['stack'][-1].type in (MAP, B_SEQ, F_SEQ)
context['stack'].pop()
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)
if context['stack'][-1].type == VAL:
context['stack'].pop()
assert context['stack'][-1].type == KEY
context['stack'].pop()
elif isinstance(token, yaml.ValueToken):
assert context['stack'][-1].type == KEY
# Discard empty values
if isinstance(next, (yaml.BlockEndToken,
yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken,
yaml.KeyToken)):
context['stack'].pop()
else:
if context['stack'][-1].explicit_key:
# ? k
# : value
# or
# ? k
# :
# value
indent = context['stack'][-1].indent + conf['spaces']
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 conf['indent-sequences'] is False:
indent = context['stack'][-1].indent
elif conf['indent-sequences'] is True:
indent = context['stack'][-1].indent + conf['spaces']
else: # 'whatever'
if next.start_mark.column == context['stack'][-1].indent:
# key:
# - e1
# - e2
indent = context['stack'][-1].indent
else:
# key:
# - e1
# - e2
indent = context['stack'][-1].indent + conf['spaces']
else:
# k:
# value
indent = context['stack'][-1].indent + conf['spaces']
context['stack'].append(Parent(VAL, indent))

View File

@@ -14,6 +14,33 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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.
.. 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.
"""
from yamllint.errors import LintProblem from yamllint.errors import LintProblem

View File

@@ -14,6 +14,16 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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
<http://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.errors import LintProblem from yamllint.errors import LintProblem

View File

@@ -14,12 +14,22 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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 use UNIX-typed new line characters (``\\n``), or
``dos`` to use DOS-typed new line characters (``\\r\\n``).
"""
from yamllint.errors import LintProblem from yamllint.errors import LintProblem
ID = 'new-lines' ID = 'new-lines'
TYPE = 'line' TYPE = 'line'
CONF = {'type': str} CONF = {'type': ('unix', 'dos')}
def check(conf, line): def check(conf, line):

View File

@@ -14,6 +14,29 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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 import string
from yamllint.errors import LintProblem from yamllint.errors import LintProblem