Compare commits

..

31 Commits

Author SHA1 Message Date
Adrien Vergé
a1eb9d7d2f yamllint version 0.5.2 2016-01-24 18:07:36 +01:00
Adrien Vergé
45538fb08a Doc: Explicit installation by adding sudo in README 2016-01-24 18:05:27 +01:00
Adrien Vergé
be998593dd Distribution: Create script with setup.py 2016-01-24 18:02:42 +01:00
Adrien Vergé
5ed496f471 Distribution: Remove unneeded setup_requires
With the new project layout, `pyyaml` is not needed anymore for parsing
setup.py.
2016-01-24 17:57:11 +01:00
Adrien Vergé
dbbecb5875 Refactor project layout to import yamllint alone
Currently importing yamllint recursively imports its submodules, which
finally requires having pyyaml installed. This is a problem when you
just want to import APP_VERSION from yamllint. For instance, setup.py
imports yamllint to know the version, but doesn't know yet that pyyaml
is to be installed, because it is stated in setup.py itself.

To solve this, yamllint/__init__.py will only contain constants. The
linting functions will be in yamllint/linter.py.
2016-01-24 17:48:20 +01:00
Adrien Vergé
7b147cb411 Tests: Remove Python 2.6 from CI tests
Because:

1. It is old. VERY old.

2. Some useful methods (`assertRaisesRegexp`, `assertIsInstance`) are
   only available from Python 2.7.
2016-01-24 17:39:36 +01:00
Adrien Vergé
fc108e7cee Config: Refactor to use YamlLintConfig objects 2016-01-24 17:39:27 +01:00
Adrien Vergé
792bdf99b4 yamllint version 0.5.1 2016-01-24 15:03:38 +01:00
Adrien Vergé
92798dbda9 Distribution: Add new keywords 2016-01-24 15:03:38 +01:00
Adrien Vergé
e3ebea6033 Distribution: Fix broken setup_requires
The `pyyaml` dependency is needed in `install_requires` but also in
`setup_requires`, because running `setup.py` requires importing
`yamllint`, which itself imports `yaml`.
2016-01-24 14:59:32 +01:00
Adrien Vergé
7983c66093 Doc: Clarify Python compatibility in README 2016-01-23 14:32:02 +01:00
Adrien Vergé
fee72d484e Doc: Add a screenshot 2016-01-23 14:30:24 +01:00
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
43 changed files with 2313 additions and 436 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,7 +1,6 @@
--- ---
language: python language: python
python: python:
- 2.6
- 2.7 - 2.7
- 3.3 - 3.3
- 3.4 - 3.4

View File

@@ -1,66 +0,0 @@
# yamllint
A linter for YAML files.
[![Build Status](https://travis-ci.org/adrienverge/yamllint.svg?branch=master)](https://travis-ci.org/adrienverge/yamllint)
[![Coverage Status](https://coveralls.io/repos/adrienverge/yamllint/badge.svg?branch=master&service=github)](https://coveralls.io/github/adrienverge/yamllint?branch=master)
Compatible with Python 2 & 3.
## Usage
```sh
yamllint my_file.yml my_other_file.yaml ...
```
```sh
yamllint -c ~/myconfig my_file.yml
```
```sh
# To output a format parsable (by editors like Vim, emacs, etc.)
yamllint -f parsable my_file.yml
```
## Installation
```sh
pip install yamllint
```
## Configuration
There is no documentation yet, so here is what you need to know: you can
override some rules, disable them or pass them in *warning* (instead of
*error*). Have a look at the content of `yamllint/conf/default.yml` and create
your own configuration file.
It could look like this:
```yaml
# This is my first, very own configuration file for yamllint!
# It extends the default conf by adjusting some options.
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
# don't bother me with this rule
comments-indentation: disable
```
Tip: if you have a `.yamllint` file in your working directory, it will be
automatically loaded as configuration by yamllint.

62
README.rst Normal file
View File

@@ -0,0 +1,62 @@
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
Written in Python (compatible with Python 2 & 3).
Documentation
-------------
http://yamllint.readthedocs.org/
Short overview
--------------
Screenshot
^^^^^^^^^^
.. image:: docs/screenshot.png
:alt: yamllint screenshot
Installation
^^^^^^^^^^^^
.. code:: bash
sudo 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

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).

8
docs/development.rst Normal file
View File

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

27
docs/index.rst Normal file
View File

@@ -0,0 +1,27 @@
yamllint documentation
======================
A linter for YAML files.
Screenshot
----------
.. image:: screenshot.png
:alt: yamllint screenshot
.. note::
The default output format is inspired by `eslint <http://eslint.org/>`_, a
great linting tool for Javascript.
Table of contents
-----------------
.. toctree::
:maxdepth: 2
quickstart
configuration
rules
development
text_editors

72
docs/quickstart.rst Normal file
View File

@@ -0,0 +1,72 @@
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):
::
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.

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

BIN
docs/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

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

@@ -26,7 +26,7 @@ setup(
author=__author__, author=__author__,
description=APP_DESCRIPTION, description=APP_DESCRIPTION,
license=__license__, license=__license__,
keywords=['yaml', 'lint', 'linter'], keywords=['yaml', 'lint', 'linter', 'syntax', 'checker'],
url='https://github.com/adrienverge/yamllint', url='https://github.com/adrienverge/yamllint',
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
@@ -42,7 +42,7 @@ setup(
], ],
packages=find_packages(), packages=find_packages(),
scripts=['bin/yamllint'], entry_points={'console_scripts': ['yamllint=yamllint.cli:run']},
package_data={'yamllint': ['conf/*.yml']}, package_data={'yamllint': ['conf/*.yml']},
install_requires=['pyyaml'], install_requires=['pyyaml'],
tests_require=['nose'], tests_require=['nose'],

View File

@@ -18,9 +18,8 @@ import unittest
import yaml import yaml
from yamllint.config import parse_config from yamllint.config import YamlLintConfig
from yamllint.errors import LintProblem from yamllint import linter
from yamllint import lint
class RuleTestCase(unittest.TestCase): class RuleTestCase(unittest.TestCase):
@@ -31,7 +30,7 @@ class RuleTestCase(unittest.TestCase):
conf = yaml.safe_load(conf) conf = yaml.safe_load(conf)
conf = {'extends': 'default', conf = {'extends': 'default',
'rules': conf} 'rules': conf}
return parse_config(yaml.safe_dump(conf)) return YamlLintConfig(yaml.safe_dump(conf))
def check(self, source, conf, **kwargs): def check(self, source, conf, **kwargs):
expected_problems = [] expected_problems = []
@@ -44,9 +43,9 @@ class RuleTestCase(unittest.TestCase):
rule_id = kwargs[key][2] rule_id = kwargs[key][2]
else: else:
rule_id = self.rule_id rule_id = self.rule_id
expected_problems.append( expected_problems.append(linter.LintProblem(
LintProblem(kwargs[key][0], kwargs[key][1], rule=rule_id)) 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(linter.run(source, self.build_fake_config(conf)))
self.assertEqual(real_problems, expected_problems) self.assertEqual(real_problems, expected_problems)

View File

@@ -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}'

View File

@@ -131,3 +131,19 @@ class CommentsTestCase(RuleTestCase):
' require-starting-space: yes\n' ' require-starting-space: yes\n'
' min-spaces-from-content: 2\n') ' min-spaces-from-content: 2\n')
self.check('# comment\n', conf) 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

@@ -452,3 +452,418 @@ class IndentationTestCase(RuleTestCase):
' 2,\n' ' 2,\n'
' 3\n' ' 3\n'
']\n', conf, problem1=(4, 4), problem2=(5, 2)) ']\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

@@ -14,56 +14,164 @@
# 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 os
import unittest import unittest
from yamllint import config from yamllint import config
class ConfigTestCase(unittest.TestCase): class SimpleConfigTestCase(unittest.TestCase):
def setUp(self): def test_parse_config(self):
self.base = config.parse_config_from_file(os.path.join( new = config.YamlLintConfig('rules:\n'
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), ' colons:\n'
'yamllint', 'conf', 'default.yml')) ' max-spaces-before: 0\n'
' max-spaces-after: 1\n')
self.assertEqual(list(new.rules.keys()), ['colons'])
self.assertEqual(new.rules['colons']['max-spaces-before'], 0)
self.assertEqual(new.rules['colons']['max-spaces-after'], 1)
self.assertEqual(len(new.enabled_rules()), 1)
def test_unknown_rule(self):
with self.assertRaisesRegexp(
config.YamlLintConfigError,
'invalid config: no such rule: "this-one-does-not-exist"'):
config.YamlLintConfig('rules:\n'
' this-one-does-not-exist: {}\n')
def test_missing_option(self):
with self.assertRaisesRegexp(
config.YamlLintConfigError,
'invalid config: missing option "max-spaces-before" '
'for rule "colons"'):
config.YamlLintConfig('rules:\n'
' colons:\n'
' max-spaces-after: 1\n')
def test_unknown_option(self):
with self.assertRaisesRegexp(
config.YamlLintConfigError,
'invalid config: unknown option "abcdef" for rule "colons"'):
config.YamlLintConfig('rules:\n'
' colons:\n'
' max-spaces-before: 0\n'
' max-spaces-after: 1\n'
' abcdef: yes\n')
class ExtendedConfigTestCase(unittest.TestCase):
def test_extend_add_rule(self):
old = config.YamlLintConfig('rules:\n'
' colons:\n'
' max-spaces-before: 0\n'
' max-spaces-after: 1\n')
new = config.YamlLintConfig('rules:\n'
' hyphens:\n'
' max-spaces-after: 2\n')
new.extend(old)
self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens'])
self.assertEqual(new.rules['colons']['max-spaces-before'], 0)
self.assertEqual(new.rules['colons']['max-spaces-after'], 1)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2)
self.assertEqual(len(new.enabled_rules()), 2)
def test_extend_remove_rule(self):
old = config.YamlLintConfig('rules:\n'
' colons:\n'
' max-spaces-before: 0\n'
' max-spaces-after: 1\n'
' hyphens:\n'
' max-spaces-after: 2\n')
new = config.YamlLintConfig('rules:\n'
' colons: disable\n')
new.extend(old)
self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens'])
self.assertEqual(new.rules['colons'], False)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2)
self.assertEqual(len(new.enabled_rules()), 1)
def test_extend_edit_rule(self):
old = config.YamlLintConfig('rules:\n'
' colons:\n'
' max-spaces-before: 0\n'
' max-spaces-after: 1\n'
' hyphens:\n'
' max-spaces-after: 2\n')
new = config.YamlLintConfig('rules:\n'
' colons:\n'
' max-spaces-before: 3\n'
' max-spaces-after: 4\n')
new.extend(old)
self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens'])
self.assertEqual(new.rules['colons']['max-spaces-before'], 3)
self.assertEqual(new.rules['colons']['max-spaces-after'], 4)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2)
self.assertEqual(len(new.enabled_rules()), 2)
def test_extend_reenable_rule(self):
old = config.YamlLintConfig('rules:\n'
' colons:\n'
' max-spaces-before: 0\n'
' max-spaces-after: 1\n'
' hyphens: disable\n')
new = config.YamlLintConfig('rules:\n'
' hyphens:\n'
' max-spaces-after: 2\n')
new.extend(old)
self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens'])
self.assertEqual(new.rules['colons']['max-spaces-before'], 0)
self.assertEqual(new.rules['colons']['max-spaces-after'], 1)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2)
self.assertEqual(len(new.enabled_rules()), 2)
class ExtendedLibraryConfigTestCase(unittest.TestCase):
def test_extend_config_disable_rule(self): def test_extend_config_disable_rule(self):
new = config.parse_config('extends: default\n' old = config.YamlLintConfig('extends: default')
'rules:\n' new = config.YamlLintConfig('extends: default\n'
' trailing-spaces: disable\n') 'rules:\n'
' trailing-spaces: disable\n')
base = self.base.copy() old.rules['trailing-spaces'] = False
del base['trailing-spaces']
self.assertEqual(sorted(new.keys()), sorted(base.keys())) self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
for rule in new: for rule in new.rules:
self.assertEqual(new[rule], base[rule]) self.assertEqual(new.rules[rule], old.rules[rule])
def test_extend_config_override_whole_rule(self): def test_extend_config_override_whole_rule(self):
new = config.parse_config('extends: default\n' old = config.YamlLintConfig('extends: default')
'rules:\n' new = config.YamlLintConfig('extends: default\n'
' empty-lines:\n' 'rules:\n'
' max: 42\n' ' empty-lines:\n'
' max-start: 43\n' ' max: 42\n'
' max-end: 44\n') ' max-start: 43\n'
' max-end: 44\n')
base = self.base.copy() old.rules['empty-lines']['max'] = 42
base['empty-lines']['max'] = 42 old.rules['empty-lines']['max-start'] = 43
base['empty-lines']['max-start'] = 43 old.rules['empty-lines']['max-end'] = 44
base['empty-lines']['max-end'] = 44
self.assertEqual(sorted(new.keys()), sorted(base.keys())) self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
for rule in new: for rule in new.rules:
self.assertEqual(new[rule], base[rule]) self.assertEqual(new.rules[rule], old.rules[rule])
def test_extend_config_override_rule_partly(self): def test_extend_config_override_rule_partly(self):
new = config.parse_config('extends: default\n' old = config.YamlLintConfig('extends: default')
'rules:\n' new = config.YamlLintConfig('extends: default\n'
' empty-lines:\n' 'rules:\n'
' max-start: 42\n') ' empty-lines:\n'
' max-start: 42\n')
base = self.base.copy() old.rules['empty-lines']['max-start'] = 42
base['empty-lines']['max-start'] = 42
self.assertEqual(sorted(new.keys()), sorted(base.keys())) self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
for rule in new: for rule in new.rules:
self.assertEqual(new[rule], base[rule]) self.assertEqual(new.rules[rule], old.rules[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.assertTrue(isinstance(e[0].curr, yaml.Token)) self.assertIsInstance(e[0].curr, yaml.Token)
self.assertTrue(isinstance(e[0].next, yaml.Token)) self.assertIsInstance(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.assertTrue(isinstance(e[3].curr, yaml.KeyToken)) self.assertIsInstance(e[3].curr, yaml.KeyToken)
self.assertTrue(isinstance(e[5].curr, yaml.ValueToken)) self.assertIsInstance(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.assertTrue(isinstance(e[0], Token)) self.assertIsInstance(e[0], Token)
self.assertTrue(isinstance(e[0].curr, yaml.StreamStartToken)) self.assertIsInstance(e[0].curr, yaml.StreamStartToken)
self.assertTrue(isinstance(e[1], Token)) self.assertIsInstance(e[1], Token)
self.assertTrue(isinstance(e[1].curr, yaml.DocumentStartToken)) self.assertIsInstance(e[1].curr, yaml.DocumentStartToken)
self.assertTrue(isinstance(e[2], Line)) self.assertIsInstance(e[2], Line)
self.assertTrue(isinstance(e[3].curr, yaml.BlockMappingStartToken)) self.assertIsInstance(e[3].curr, yaml.BlockMappingStartToken)
self.assertTrue(isinstance(e[4].curr, yaml.KeyToken)) self.assertIsInstance(e[4].curr, yaml.KeyToken)
self.assertTrue(isinstance(e[6].curr, yaml.ValueToken)) self.assertIsInstance(e[6].curr, yaml.ValueToken)
self.assertTrue(isinstance(e[8], Line)) self.assertIsInstance(e[8], Line)
self.assertTrue(isinstance(e[11], Line)) self.assertIsInstance(e[11], Line)

View File

@@ -14,104 +14,11 @@
# 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.errors import LintProblem
from yamllint import parser
APP_NAME = 'yamllint' APP_NAME = 'yamllint'
APP_VERSION = '0.3.0' APP_VERSION = '0.5.2'
APP_DESCRIPTION = 'A linter for 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 get_costemic_problems(buffer, conf):
rules = config.get_enabled_rules(conf)
# Split token rules from line rules
token_rules = [r for r in rules if r.TYPE == 'token']
line_rules = [r for r in rules if r.TYPE == 'line']
context = {}
for rule in token_rules:
context[rule.ID] = {}
for elem in parser.token_or_line_generator(buffer):
if isinstance(elem, parser.Token):
for rule in token_rules:
rule_conf = conf[rule.ID]
for problem in rule.check(rule_conf,
elem.curr, elem.prev, elem.next,
context[rule.ID]):
problem.rule = rule.ID
problem.level = rule_conf['level']
yield problem
elif isinstance(elem, parser.Line):
for rule in line_rules:
rule_conf = conf[rule.ID]
for problem in rule.check(rule_conf, elem):
problem.rule = rule.ID
problem.level = rule_conf['level']
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):
"""Lints a YAML source.
Returns a generator of LintProblem objects.
:param input: buffer, string or stream to read from
:param conf: yamllint configuration object
"""
if type(input) == str:
return _lint(input, conf)
elif hasattr(input, 'read'): # Python 2's file or Python 3's io.IOBase
# We need to have everything in memory to parse correctly
content = input.read()
return _lint(content, conf)
else:
raise TypeError('input should be a string or a stream')

76
bin/yamllint → yamllint/cli.py Executable file → Normal file
View File

@@ -22,16 +22,50 @@ import sys
import argparse import argparse
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
from yamllint import config from yamllint.config import YamlLintConfig, YamlLintConfigError
from yamllint.errors import YamlLintConfigError from yamllint import linter
from yamllint import lint
from yamllint import output
if __name__ == '__main__': 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=None):
parser = argparse.ArgumentParser(prog=APP_NAME, parser = argparse.ArgumentParser(prog=APP_NAME,
description=APP_DESCRIPTION) description=APP_DESCRIPTION)
parser.add_argument('files', metavar='FILES', nargs='+', parser.add_argument('files', metavar='FILE_OR_DIR', nargs='+',
help='files to check') help='files to check')
parser.add_argument('-c', '--config', dest='config_file', action='store', parser.add_argument('-c', '--config', dest='config_file', action='store',
help='path to a custom configuration') help='path to a custom configuration')
@@ -43,40 +77,42 @@ if __name__ == '__main__':
# TODO: read from stdin when no filename? # TODO: read from stdin when no filename?
args = parser.parse_args() args = parser.parse_args(argv)
try: try:
if args.config_file is not None: if args.config_file is not None:
conf = config.parse_config_from_file(args.config_file) conf = YamlLintConfig(file=args.config_file)
elif os.path.isfile('.yamllint'): elif os.path.isfile('.yamllint'):
conf = config.parse_config_from_file('.yamllint') conf = YamlLintConfig(file='.yamllint')
else: else:
conf = config.parse_config('extends: default') conf = YamlLintConfig('extends: default')
except YamlLintConfigError as e: except YamlLintConfigError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(-1) sys.exit(-1)
return_code = 0 return_code = 0
for file in args.files: for file in find_files_recursively(args.files):
if args.format != 'parsable':
print('\033[4m%s\033[0m' % file)
try: try:
first = True
with open(file) as f: with open(file) as f:
for problem in lint(f, conf): for problem in linter.run(f, conf):
if args.format == 'parsable': if args.format == 'parsable':
print(output.parsable_format(problem, file)) print(Format.parsable(problem, file))
else: else:
print(output.standard_format(problem, file)) if first:
print('\033[4m%s\033[0m' % file)
first = False
print(Format.standard(problem, file))
if return_code == 0 and problem.level == 'error': if return_code == 0 and problem.level == 'error':
return_code = 1 return_code = 1
if not first and args.format != 'parsable':
print('')
except EnvironmentError as e: except EnvironmentError as e:
print(e) print(e)
return_code = -1 return_code = -1
if args.format != 'parsable':
print('')
sys.exit(return_code) sys.exit(return_code)

View File

@@ -32,6 +32,7 @@ rules:
indentation: indentation:
spaces: 2 spaces: 2
indent-sequences: 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}

View File

@@ -19,10 +19,109 @@ import os.path
import yaml import yaml
import yamllint.rules import yamllint.rules
from yamllint.errors import YamlLintConfigError
def get_extended_conf(name): class YamlLintConfigError(Exception):
pass
class YamlLintConfig(object):
def __init__(self, content=None, file=None):
assert (content is None) ^ (file is None)
if file is not None:
with open(file) as f:
content = f.read()
self.parse(content)
self.validate()
def enabled_rules(self):
return [yamllint.rules.get(id) for id, val in self.rules.items()
if val is not False]
def extend(self, base_config):
assert isinstance(base_config, YamlLintConfig)
for rule in self.rules:
if (type(self.rules[rule]) == dict and
rule in base_config.rules and
base_config.rules[rule] is not False):
base_config.rules[rule].update(self.rules[rule])
else:
base_config.rules[rule] = self.rules[rule]
self.rules = base_config.rules
def parse(self, raw_content):
try:
conf = yaml.safe_load(raw_content)
except Exception as e:
raise YamlLintConfigError('invalid config: %s' % e)
self.rules = conf.get('rules', {})
# Does this conf override another conf that we need to load?
if 'extends' in conf:
path = get_extended_config_file(conf['extends'])
base = YamlLintConfig(file=path)
try:
self.extend(base)
except Exception as e:
raise YamlLintConfigError('invalid config: %s' % e)
def validate(self):
for id in self.rules:
try:
rule = yamllint.rules.get(id)
except Exception as e:
raise YamlLintConfigError('invalid config: %s' % e)
self.rules[id] = validate_rule_conf(rule, self.rules[id])
def validate_rule_conf(rule, conf):
if conf is False or conf == 'disable':
return False
if type(conf) == dict:
if 'level' not in conf:
conf['level'] = 'error'
elif conf['level'] not in ('error', 'warning'):
raise YamlLintConfigError(
'invalid config: level should be "error" or "warning"')
options = getattr(rule, 'CONF', {})
for optkey in conf:
if optkey == 'level':
continue
if optkey not in options:
raise YamlLintConfigError(
'invalid config: unknown option "%s" for rule "%s"' %
(optkey, rule.ID))
if type(options[optkey]) == tuple:
if conf[optkey] not in options[optkey]:
raise YamlLintConfigError(
'invalid config: option "%s" of "%s" should be in %s'
% (optkey, rule.ID, options[optkey]))
else:
if type(conf[optkey]) != options[optkey]:
raise YamlLintConfigError(
'invalid config: option "%s" of "%s" should be %s'
% (optkey, rule.ID, options[optkey].__name__))
for optkey in options:
if optkey not in conf:
raise YamlLintConfigError(
'invalid config: missing option "%s" for rule "%s"' %
(optkey, rule.ID))
else:
raise YamlLintConfigError(('invalid config: rule "%s": should be '
'either "disable" or a dict') % rule.ID)
return conf
def get_extended_config_file(name):
# Is it a standard conf shipped with yamllint... # Is it a standard conf shipped with yamllint...
if '/' not in name: if '/' not in name:
std_conf = os.path.join(os.path.dirname(os.path.realpath(__file__)), std_conf = os.path.join(os.path.dirname(os.path.realpath(__file__)),
@@ -33,81 +132,3 @@ def get_extended_conf(name):
# or a custom conf on filesystem? # or a custom conf on filesystem?
return name return name
def extend_config(content):
try:
conf = yaml.safe_load(content)
if 'rules' not in conf:
conf['rules'] = {}
# Does this conf override another conf that we need to load?
if 'extends' in conf:
base = parse_config_from_file(get_extended_conf(conf['extends']))
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
return conf
except Exception as e:
raise YamlLintConfigError('invalid config: %s' % e)
def parse_config(content):
conf = extend_config(content)
rules = {}
for id in conf['rules']:
try:
rule = yamllint.rules.get(id)
except Exception as e:
raise YamlLintConfigError('invalid config: %s' % e)
if conf['rules'][id] == 'disable':
continue
rules[id] = {'level': 'error'}
if type(conf['rules'][id]) == dict:
if 'level' in conf['rules'][id]:
if conf['rules'][id]['level'] not in ('error', 'warning'):
raise YamlLintConfigError(
'invalid config: level should be "error" or "warning"')
rules[id]['level'] = conf['rules'][id]['level']
options = getattr(rule, 'CONF', {})
for optkey in conf['rules'][id]:
if optkey == 'level':
continue
if optkey not in options:
raise YamlLintConfigError(
'invalid config: unknown option "%s" for rule "%s"' %
(optkey, id))
if type(options[optkey]) == tuple:
if conf['rules'][id][optkey] not in options[optkey]:
raise YamlLintConfigError(
('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]
else:
raise YamlLintConfigError(('invalid config: rule "%s": should be '
'either "disable" or a dict') % id)
return rules
def parse_config_from_file(path):
with open(path) as f:
return parse_config(f.read())
def get_enabled_rules(conf):
return [yamllint.rules.get(r) for r in conf.keys() if conf[r] is not False]

View File

@@ -1,50 +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/>.
class LintProblem(object):
def __init__(self, line, column, desc='<no description>', rule=None):
self.line = line
self.column = column
self.desc = desc
self.rule = rule
self.level = None
@property
def message(self):
if self.rule is not None:
return '%s (%s)' % (self.desc, self.rule)
return self.desc
def __eq__(self, other):
return (self.line == other.line and
self.column == other.column and
self.rule == other.rule)
def __lt__(self, other):
return (self.line < other.line or
(self.line == other.line and self.column < other.column))
def __repr__(self):
return '%d:%d: %s' % (self.line, self.column, self.message)
class YamlLintError(Exception):
pass
class YamlLintConfigError(YamlLintError):
pass

137
yamllint/linter.py Normal file
View File

@@ -0,0 +1,137 @@
# -*- 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 import parser
class LintProblem(object):
"""Represents a linting problem found by yamllint."""
def __init__(self, line, column, desc='<no description>', rule=None):
#: Line on which the problem was found (starting at 1)
self.line = line
#: Column on which the problem was found (starting at 1)
self.column = column
#: Human-readable description of the problem
self.desc = desc
#: Identifier of the rule that detected the problem
self.rule = rule
self.level = None
@property
def message(self):
if self.rule is not None:
return '%s (%s)' % (self.desc, self.rule)
return self.desc
def __eq__(self, other):
return (self.line == other.line and
self.column == other.column and
self.rule == other.rule)
def __lt__(self, other):
return (self.line < other.line or
(self.line == other.line and self.column < other.column))
def __repr__(self):
return '%d:%d: %s' % (self.line, self.column, self.message)
def get_costemic_problems(buffer, conf):
rules = conf.enabled_rules()
# Split token rules from line rules
token_rules = [r for r in rules if r.TYPE == 'token']
line_rules = [r for r in rules if r.TYPE == 'line']
context = {}
for rule in token_rules:
context[rule.ID] = {}
for elem in parser.token_or_line_generator(buffer):
if isinstance(elem, parser.Token):
for rule in token_rules:
rule_conf = conf.rules[rule.ID]
for problem in rule.check(rule_conf,
elem.curr, elem.prev, elem.next,
context[rule.ID]):
problem.rule = rule.ID
problem.level = rule_conf['level']
yield problem
elif isinstance(elem, parser.Line):
for rule in line_rules:
rule_conf = conf.rules[rule.ID]
for problem in rule.check(rule_conf, elem):
problem.rule = rule.ID
problem.level = rule_conf['level']
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 _run(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 run(input, conf):
"""Lints a YAML source.
Returns a generator of LintProblem objects.
:param input: buffer, string or stream to read from
:param conf: yamllint configuration object
"""
if type(input) == str:
return _run(input, conf)
elif hasattr(input, 'read'): # Python 2's file or Python 3's io.IOBase
# We need to have everything in memory to parse correctly
content = input.read()
return _run(content, conf)
else:
raise TypeError('input should be a string or a stream')

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

@@ -14,6 +14,54 @@
# 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 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 import yaml
from yamllint.rules.common import spaces_after, spaces_before from yamllint.rules.common import spaces_after, spaces_before

View File

@@ -14,6 +14,55 @@
# 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 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 import yaml
from yamllint.rules.common import spaces_after, spaces_before from yamllint.rules.common import spaces_after, spaces_before

View File

@@ -14,6 +14,62 @@
# 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.rules.common import spaces_after, spaces_before, is_explicit_key from yamllint.rules.common import spaces_after, spaces_before, is_explicit_key

View File

@@ -14,9 +14,57 @@
# 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 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 import yaml
from yamllint.errors import LintProblem from yamllint.linter import LintProblem
from yamllint.rules.common import spaces_after, spaces_before from yamllint.rules.common import spaces_after, spaces_before

View File

@@ -14,9 +14,50 @@
# 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 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 import yaml
from yamllint.errors import LintProblem from yamllint.linter import LintProblem
from yamllint.rules.common import get_comments_between_tokens from yamllint.rules.common import get_comments_between_tokens
@@ -30,11 +71,13 @@ def check(conf, token, prev, next, context):
for comment in get_comments_between_tokens(token, next): for comment in get_comments_between_tokens(token, next):
if (conf['min-spaces-from-content'] != -1 and if (conf['min-spaces-from-content'] != -1 and
not isinstance(token, yaml.StreamStartToken) and not isinstance(token, yaml.StreamStartToken) and
comment.line == token.end_mark.line + 1 and comment.line == token.end_mark.line + 1):
comment.pointer - token.end_mark.pointer < # Sometimes token end marks are on the next line
conf['min-spaces-from-content']): if token.end_mark.buffer[token.end_mark.pointer - 1] != '\n':
yield LintProblem(comment.line, comment.column, if (comment.pointer - token.end_mark.pointer <
'too few spaces before comment') conf['min-spaces-from-content']):
yield LintProblem(comment.line, comment.column,
'too few spaces before comment')
if (conf['require-starting-space'] and if (conf['require-starting-space'] and
comment.pointer + 1 < len(comment.buffer) and comment.pointer + 1 < len(comment.buffer) and

View File

@@ -14,9 +14,70 @@
# 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 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 import yaml
from yamllint.errors import LintProblem from yamllint.linter import LintProblem
from yamllint.rules.common import get_line_indent, get_comments_between_tokens from yamllint.rules.common import get_line_indent, get_comments_between_tokens

View File

@@ -16,7 +16,7 @@
import yaml import yaml
from yamllint.errors import LintProblem from yamllint.linter import LintProblem
def spaces_after(token, prev, next, min=-1, max=-1, def spaces_after(token, prev, next, min=-1, max=-1,
@@ -33,7 +33,10 @@ def spaces_after(token, prev, next, min=-1, max=-1,
def spaces_before(token, prev, next, min=-1, max=-1, def spaces_before(token, prev, next, min=-1, max=-1,
min_desc=None, max_desc=None): min_desc=None, max_desc=None):
if prev is not None and prev.end_mark.line == token.start_mark.line: 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 spaces = token.start_mark.pointer - prev.end_mark.pointer
if max != - 1 and spaces > max: if max != - 1 and spaces > max:
return LintProblem(token.start_mark.line + 1, return LintProblem(token.start_mark.line + 1,

View File

@@ -14,9 +14,69 @@
# 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.linter import LintProblem
ID = 'document-end' ID = 'document-end'

View File

@@ -14,9 +14,59 @@
# 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.linter import LintProblem
ID = 'document-start' ID = 'document-start'

View File

@@ -14,7 +14,43 @@
# 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 yamllint.errors import LintProblem """
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.linter import LintProblem
ID = 'empty-lines' ID = 'empty-lines'

View File

@@ -14,6 +14,60 @@
# 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.rules.common import spaces_after from yamllint.rules.common import spaces_after

View File

@@ -14,16 +14,133 @@
# 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.linter import LintProblem
from yamllint.rules.common import is_explicit_key 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')} 'indent-sequences': (True, False, 'whatever'),
'check-multi-line-strings': bool}
ROOT, MAP, B_SEQ, F_SEQ, KEY, VAL = range(6) ROOT, MAP, B_SEQ, F_SEQ, KEY, VAL = range(6)
@@ -35,6 +152,76 @@ class Parent(object):
self.explicit_key = False self.explicit_key = False
def check_scalar_indentation(conf, token, context):
if token.start_mark.line == token.end_mark.line:
return
if token.plain:
expected_indent = token.start_mark.column
elif token.style in ('"', "'"):
expected_indent = token.start_mark.column + 1
elif token.style in ('>', '|'):
if context['stack'][-1].type == B_SEQ:
# - >
# multi
# 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']
line_no = token.start_mark.line + 1
line_start = token.start_mark.pointer
while True:
line_start = token.start_mark.buffer.find(
'\n', line_start, token.end_mark.pointer - 1) + 1
if line_start == 0:
break
line_no += 1
indent = 0
while token.start_mark.buffer[line_start + indent] == ' ':
indent += 1
if token.start_mark.buffer[line_start + indent] == '\n':
continue
if indent < expected_indent:
yield LintProblem(line_no, indent + 1,
('wrong indentation: expected at least %d but '
'found %d') % (expected_indent, indent))
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))
def check(conf, token, prev, next, context): def check(conf, token, prev, next, context):
if 'stack' not in context: if 'stack' not in context:
context['stack'] = [Parent(ROOT, 0)] context['stack'] = [Parent(ROOT, 0)]
@@ -42,11 +229,13 @@ def check(conf, token, prev, next, context):
# Step 1: Lint # Step 1: Lint
if (not isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)) and needs_lint = (
not isinstance(token, yaml.BlockEndToken) and not isinstance(token, (yaml.StreamStartToken, yaml.StreamEndToken)) and
not (isinstance(token, yaml.ScalarToken) and token.value == '') and not isinstance(token, yaml.BlockEndToken) and
token.start_mark.line + 1 > context['cur_line']): 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 found_indentation = token.start_mark.column
expected = context['stack'][-1].indent expected = context['stack'][-1].indent
@@ -63,10 +252,17 @@ def check(conf, token, prev, next, context):
'wrong indentation: expected %d but found %d' % 'wrong indentation: expected %d but found %d' %
(expected, found_indentation)) (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_indent'] = found_indentation
context['cur_line'] = token.end_mark.line + 1 context['cur_line'] = token.end_mark.line + 1
# Step 2: Update state # Step 2.b: Update state
if isinstance(token, yaml.BlockMappingStartToken): if isinstance(token, yaml.BlockMappingStartToken):
assert isinstance(next, yaml.KeyToken) assert isinstance(next, yaml.KeyToken)

View File

@@ -14,7 +14,34 @@
# 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 yamllint.errors import LintProblem """
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.linter import LintProblem
ID = 'line-length' ID = 'line-length'

View File

@@ -14,7 +14,17 @@
# 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 yamllint.errors import LintProblem """
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.linter import LintProblem
ID = 'new-line-at-end-of-file' ID = 'new-line-at-end-of-file'

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/>.
from yamllint.errors import LintProblem """
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.linter 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,9 +14,32 @@
# 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.linter import LintProblem
ID = 'trailing-spaces' ID = 'trailing-spaces'