pull/442/merge
QuentinN42 3 years ago committed by GitHub
commit f461280cf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -47,7 +47,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Append GitHub Actions system path - name: Append GitHub Actions system path
run: echo "$HOME/.local/bin" >> $GITHUB_PATH run: echo "$HOME/.local/bin" >> $GITHUB_PATH
- run: pip install coveralls - run: pip install coveralls ddt
- run: pip install . - run: pip install .
- run: coverage run --source=yamllint -m unittest discover - run: coverage run --source=yamllint -m unittest discover
- name: Coveralls - name: Coveralls

4
.gitignore vendored

@ -5,3 +5,7 @@ __pycache__
/yamllint.egg-info /yamllint.egg-info
/build /build
/.eggs /.eggs
.venv
venv
.coverage
coverage.xml

@ -14,8 +14,12 @@ Pull Request Process
.. code:: bash .. code:: bash
pip install --user . pip install --user .
python -m unittest discover # all tests... pip install coveralls ddt
python -m unittest tests/rules/test_commas.py # or just some tests (faster) # all tests...
python -m coverage run --source=yamllint -m unittest discover
coverage report
# or just some tests (faster)
python -m unittest tests/rules/test_commas.py
3. If you add code that should be tested, add tests. 3. If you add code that should be tested, add tests.

@ -314,6 +314,9 @@ class CommandLineTestCase(unittest.TestCase):
config = os.path.join(dir, 'config') config = os.path.join(dir, 'config')
self.addCleanup(os.environ.update, HOME=os.environ['HOME']) self.addCleanup(os.environ.update, HOME=os.environ['HOME'])
# remove other env vars to make sure we are using the HOME config file.
os.environ.pop('YAMLLINT_CONFIG_FILE', None)
os.environ.pop('XDG_CONFIG_HOME', None)
os.environ['HOME'] = home os.environ['HOME'] = home
with open(config, 'w') as f: with open(config, 'w') as f:

@ -0,0 +1,270 @@
# -*- coding: utf-8 -*-
# flake8: noqa
import unittest
import string
import ddt
from yamllint.linter import LintProblem
from yamllint.format import (
escape_xml,
severity_from_level,
max_level,
Formater,
ParsableFormater,
GithubFormater,
ColoredFormater,
StandardFormater,
JSONFormater,
JunitFormater,
CodeclimateFormater
)
@ddt.ddt
class TextToXMLTestCase(unittest.TestCase):
def test_letters_chars(self):
txt = string.ascii_letters
self.assertEqual(escape_xml(txt), txt)
@ddt.data(
('&', '&'),
('<', '&lt;'),
('>', '&gt;'),
('"', '&quot;'),
("'", '&apos;'),
("too many blank lines (3 > 2)", 'too many blank lines (3 &gt; 2)'),
('line too long (100 > 80 characters)', 'line too long (100 &gt; 80 characters)')
)
@ddt.unpack
def test_specials_chars(self, inp, out):
self.assertEqual(escape_xml(inp), out)
@ddt.ddt
class CodeClimateSeverityTestCase(unittest.TestCase):
@ddt.data(
(None, "info"),
('warning', "minor"),
('error', "major"),
(0, "info"),
(1, "minor"),
(2, "major"),
)
@ddt.unpack
def test_specials_chars(self, inp, out):
self.assertEqual(severity_from_level(inp), out)
@ddt.ddt
class FormaterTestCase(unittest.TestCase):
def test_get_formaters_names(self):
self.assertEqual(
set(Formater.get_formaters_names()),
{
"parsable",
"github",
"colored",
"standard",
"json",
"junitxml",
"codeclimate"
}
)
@ddt.data(
("parsable", ParsableFormater, True),
("github", GithubFormater, True),
("colored", ColoredFormater, True),
("standard", StandardFormater, True),
("json", JSONFormater, True),
("junitxml", JunitFormater, True),
("codeclimate", CodeclimateFormater, True),
("parsable", ParsableFormater, False),
("github", GithubFormater, False),
("colored", ColoredFormater, False),
("standard", StandardFormater, False),
("json", JSONFormater, False),
("junitxml", JunitFormater, False),
("codeclimate", CodeclimateFormater, False),
)
@ddt.unpack
def test_get_formater(self, name, cls, no_warn):
res = Formater.get_formater(name, no_warn)
self.assertTrue(isinstance(res, cls))
self.assertEqual(res.no_warn, no_warn)
def test_unknown_formater(self):
with self.assertRaises(ValueError):
Formater.get_formater("unknown", False)
def test_abstract_class(self):
inst = Formater(False)
with self.assertRaises(NotImplementedError):
inst.show_problems_for_all_files([])
with self.assertRaises(NotImplementedError):
inst.show_problems_for_file([], "a")
with self.assertRaises(NotImplementedError):
inst.show_problem(None, "a")
NONE = {}
NO_ERROR = {"file1.yml": []}
ONE_NOTHING = {"file1.yml": [
LintProblem(1, 1, desc="desc of None", rule="my-rule")
]}
ONE_ERROR = {"file1.yml": [
LintProblem(
line=1,
column=2,
desc="desc of error",
rule="my-rule",
level="error"
)
]}
ONE_WARNING = {"file1.yml": [
LintProblem(
line=1,
column=2,
desc="desc of warn",
rule="my-rule",
level="warning"
)
]}
MIXED_ONE_FILE = {"file1.yml": [
ONE_NOTHING["file1.yml"][0],
ONE_ERROR["file1.yml"][0],
ONE_WARNING["file1.yml"][0]
]}
MIXED_MULT_FILE = {
"file1.yml": ONE_NOTHING["file1.yml"],
"file2.yml": ONE_ERROR["file1.yml"],
"file3.yml": ONE_WARNING["file1.yml"]
}
@ddt.ddt
class FormatersTestCase(unittest.TestCase):
@ddt.data(
# No errors
(ParsableFormater(True), NONE, ""),
(GithubFormater(True), NONE, ""),
(ColoredFormater(True), NONE, ""),
(StandardFormater(True), NONE, ""),
(JSONFormater(True), NONE, "[]\n"),
(CodeclimateFormater(True), NONE, "[]\n"),
(ParsableFormater(True), NO_ERROR, ""),
(GithubFormater(True), NO_ERROR, ""),
(ColoredFormater(True), NO_ERROR, ""),
(StandardFormater(True), NO_ERROR, ""),
(JSONFormater(True), NO_ERROR, "[]\n"),
(CodeclimateFormater(True), NO_ERROR, "[]\n"),
(ParsableFormater(True), ONE_NOTHING, ""),
(GithubFormater(True), ONE_NOTHING, ""),
(ColoredFormater(True), ONE_NOTHING, ""),
(StandardFormater(True), ONE_NOTHING, ""),
(JSONFormater(True), ONE_NOTHING, '[]\n'),
(CodeclimateFormater(True), ONE_NOTHING, '[]\n'),
# Errors with no level are ignored
(ParsableFormater(False), ONE_NOTHING, ""),
(GithubFormater(False), ONE_NOTHING, ""),
(ColoredFormater(False), ONE_NOTHING, ""),
(StandardFormater(False), ONE_NOTHING, ""),
(JSONFormater(False), ONE_NOTHING, '[]\n'),
(CodeclimateFormater(False), ONE_NOTHING, '[]\n'),
# 1 Skipped warning
(ParsableFormater(True), ONE_WARNING, ""),
(GithubFormater(True), ONE_WARNING, ""),
(ColoredFormater(True), ONE_WARNING, ""),
(StandardFormater(True), ONE_WARNING, ""),
(JSONFormater(True), ONE_WARNING, '[]\n'),
(CodeclimateFormater(True), ONE_WARNING, '[]\n'),
# 1 Unskipped warning
(ParsableFormater(False), ONE_WARNING, 'file1.yml:1:2: [warning] desc of warn (my-rule)\n'),
(GithubFormater(False), ONE_WARNING, '::group::file1.yml\n::warning file=file1.yml,line=1,col=2::1:2 [my-rule] desc of warn\n::endgroup::\n\n'),
(ColoredFormater(False), ONE_WARNING, '\x1b[4mfile1.yml\x1b[0m\n \x1b[2m1:2\x1b[0m \x1b[33mwarning\x1b[0m desc of warn \x1b[2m(my-rule)\x1b[0m\n\n'),
(StandardFormater(False), ONE_WARNING, 'file1.yml\n 1:2 warning desc of warn (my-rule)\n\n'),
(JSONFormater(False), ONE_WARNING, '[\n {\n "line": 1,\n "column": 2,\n "rule": "my-rule",\n "level": "warning",\n "message": "desc of warn",\n "path": "file1.yml"\n }\n]\n'),
(CodeclimateFormater(False), ONE_WARNING, '[\n {\n "type": "issue",\n "check_name": "my-rule",\n "description": "desc of warn",\n "content": "desc of warn (my-rule)",\n "categories": [\n "Style"\n ],\n "location": {\n "path": "file1.yml",\n "positions": {\n "begin": {\n "line": 1,\n "column": 2\n }\n }\n },\n "remediation_points": 1000,\n "severity": "minor"\n }\n]\n'),
# 1 Error
(ParsableFormater(True), ONE_ERROR, 'file1.yml:1:2: [error] desc of error (my-rule)\n'),
(GithubFormater(True), ONE_ERROR, '::group::file1.yml\n::error file=file1.yml,line=1,col=2::1:2 [my-rule] desc of error\n::endgroup::\n\n'),
(ColoredFormater(True), ONE_ERROR, '\x1b[4mfile1.yml\x1b[0m\n \x1b[2m1:2\x1b[0m \x1b[31merror\x1b[0m desc of error \x1b[2m(my-rule)\x1b[0m\n\n'),
(StandardFormater(True), ONE_ERROR, 'file1.yml\n 1:2 error desc of error (my-rule)\n\n'),
(JSONFormater(True), ONE_ERROR, '[\n {\n "line": 1,\n "column": 2,\n "rule": "my-rule",\n "level": "error",\n "message": "desc of error",\n "path": "file1.yml"\n }\n]\n'),
(CodeclimateFormater(True), ONE_ERROR, '[\n {\n "type": "issue",\n "check_name": "my-rule",\n "description": "desc of error",\n "content": "desc of error (my-rule)",\n "categories": [\n "Style"\n ],\n "location": {\n "path": "file1.yml",\n "positions": {\n "begin": {\n "line": 1,\n "column": 2\n }\n }\n },\n "remediation_points": 1000,\n "severity": "major"\n }\n]\n'),
# mixed warn / err on the same file
(ParsableFormater(False), MIXED_ONE_FILE, 'file1.yml:1:2: [error] desc of error (my-rule)\nfile1.yml:1:2: [warning] desc of warn (my-rule)\n'),
(GithubFormater(False), MIXED_ONE_FILE, '::group::file1.yml\n::error file=file1.yml,line=1,col=2::1:2 [my-rule] desc of error\n::warning file=file1.yml,line=1,col=2::1:2 [my-rule] desc of warn\n::endgroup::\n\n'),
(ColoredFormater(False), MIXED_ONE_FILE, '\x1b[4mfile1.yml\x1b[0m\n \x1b[2m1:2\x1b[0m \x1b[31merror\x1b[0m desc of error \x1b[2m(my-rule)\x1b[0m\n \x1b[2m1:2\x1b[0m \x1b[33mwarning\x1b[0m desc of warn \x1b[2m(my-rule)\x1b[0m\n\n'),
(StandardFormater(False), MIXED_ONE_FILE, 'file1.yml\n 1:2 error desc of error (my-rule)\n 1:2 warning desc of warn (my-rule)\n\n'),
(JSONFormater(False), MIXED_ONE_FILE, '[\n {\n "line": 1,\n "column": 2,\n "rule": "my-rule",\n "level": "error",\n "message": "desc of error",\n "path": "file1.yml"\n },\n {\n "line": 1,\n "column": 2,\n "rule": "my-rule",\n "level": "warning",\n "message": "desc of warn",\n "path": "file1.yml"\n }\n]\n'),
(CodeclimateFormater(False), MIXED_ONE_FILE, '[\n {\n "type": "issue",\n "check_name": "my-rule",\n "description": "desc of error",\n "content": "desc of error (my-rule)",\n "categories": [\n "Style"\n ],\n "location": {\n "path": "file1.yml",\n "positions": {\n "begin": {\n "line": 1,\n "column": 2\n }\n }\n },\n "remediation_points": 1000,\n "severity": "major"\n },\n {\n "type": "issue",\n "check_name": "my-rule",\n "description": "desc of warn",\n "content": "desc of warn (my-rule)",\n "categories": [\n "Style"\n ],\n "location": {\n "path": "file1.yml",\n "positions": {\n "begin": {\n "line": 1,\n "column": 2\n }\n }\n },\n "remediation_points": 1000,\n "severity": "minor"\n }\n]\n'),
# mixed warn / err on multiples files
(ParsableFormater(False), MIXED_MULT_FILE, 'file2.yml:1:2: [error] desc of error (my-rule)\nfile3.yml:1:2: [warning] desc of warn (my-rule)\n'),
(GithubFormater(False), MIXED_MULT_FILE, '::group::file2.yml\n::error file=file2.yml,line=1,col=2::1:2 [my-rule] desc of error\n::endgroup::\n\n::group::file3.yml\n::warning file=file3.yml,line=1,col=2::1:2 [my-rule] desc of warn\n::endgroup::\n\n'),
(ColoredFormater(False), MIXED_MULT_FILE, '\x1b[4mfile2.yml\x1b[0m\n \x1b[2m1:2\x1b[0m \x1b[31merror\x1b[0m desc of error \x1b[2m(my-rule)\x1b[0m\n\n\x1b[4mfile3.yml\x1b[0m\n \x1b[2m1:2\x1b[0m \x1b[33mwarning\x1b[0m desc of warn \x1b[2m(my-rule)\x1b[0m\n\n'),
(StandardFormater(False), MIXED_MULT_FILE, 'file2.yml\n 1:2 error desc of error (my-rule)\n\nfile3.yml\n 1:2 warning desc of warn (my-rule)\n\n'),
(JSONFormater(False), MIXED_MULT_FILE, '[\n {\n "line": 1,\n "column": 2,\n "rule": "my-rule",\n "level": "error",\n "message": "desc of error",\n "path": "file2.yml"\n },\n {\n "line": 1,\n "column": 2,\n "rule": "my-rule",\n "level": "warning",\n "message": "desc of warn",\n "path": "file3.yml"\n }\n]\n'),
(CodeclimateFormater(False), MIXED_MULT_FILE, '[\n {\n "type": "issue",\n "check_name": "my-rule",\n "description": "desc of error",\n "content": "desc of error (my-rule)",\n "categories": [\n "Style"\n ],\n "location": {\n "path": "file2.yml",\n "positions": {\n "begin": {\n "line": 1,\n "column": 2\n }\n }\n },\n "remediation_points": 1000,\n "severity": "major"\n },\n {\n "type": "issue",\n "check_name": "my-rule",\n "description": "desc of warn",\n "content": "desc of warn (my-rule)",\n "categories": [\n "Style"\n ],\n "location": {\n "path": "file3.yml",\n "positions": {\n "begin": {\n "line": 1,\n "column": 2\n }\n }\n },\n "remediation_points": 1000,\n "severity": "minor"\n }\n]\n'),
)
@ddt.unpack
def test_all_formaters(self, inst, inp, ret):
self.assertEqual(
inst.show_problems_for_all_files(inp),
ret
)
@ddt.ddt
class MaxLevelTestCase(unittest.TestCase):
@ddt.data(
(NONE, 0),
(NO_ERROR, 0),
(ONE_NOTHING, 0),
(ONE_ERROR, 2),
(ONE_WARNING, 1),
(MIXED_ONE_FILE, 2),
(MIXED_MULT_FILE, 2),
)
@ddt.unpack
def test_all_formaters(self, inp, ret):
self.assertEqual(max_level(inp), ret)
@ddt.ddt
class JunitTestCase(unittest.TestCase):
@ddt.data(
(NONE, False, [], ['<\/error><\/testcase>', '<\/failure><\/testcase>'], 7),
(NO_ERROR, False, [], ['<\/error><\/testcase>', '<\/failure><\/testcase>'], 7),
(ONE_NOTHING, False, [], ['<\/error><\/testcase>', '<\/failure><\/testcase>'], 7),
(ONE_ERROR, False, ['<\/error><\/testcase>'], ['<\/failure><\/testcase>'], 7),
(ONE_WARNING, False, ['<\/failure><\/testcase>'], ['<\/error><\/testcase>'], 7),
(ONE_WARNING, True, [], ['<\/error><\/testcase>', '<\/failure><\/testcase>'], 7),
(MIXED_ONE_FILE, False, ['<\/error><\/testcase>', '<\/failure><\/testcase>'], [], 8),
(MIXED_MULT_FILE, False, ['<\/error><\/testcase>', '<\/failure><\/testcase>'], [], 8),
)
@ddt.unpack
def test_all_formaters(self, inp, no_warn, contain, not_contain, length):
res = JunitFormater(no_warn).show_problems_for_all_files(inp)
self.assertTrue(res.startswith(
'<?xml version="1.0" encoding="utf-8"?>\n' \
'<testsuites>\n' \
' <testsuite name="yamllint" '
))
for e in contain:
self.assertTrue(e in res)
for e in not_contain:
self.assertFalse(e in res)
self.assertTrue(res.endswith(
'\n' \
' </testsuite>\n' \
'</testsuites>\n'))
self.assertEqual(len(res.split('\n')), length)

@ -17,13 +17,13 @@ import argparse
import io import io
import locale import locale
import os import os
import platform
import sys import sys
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
from yamllint import linter from yamllint import linter
from yamllint.config import YamlLintConfig, YamlLintConfigError from yamllint.config import YamlLintConfig, YamlLintConfigError
from yamllint.linter import PROBLEM_LEVELS from yamllint.linter import PROBLEM_LEVELS
from yamllint.format import show_all_problems, Formater
def find_files_recursively(items, conf): def find_files_recursively(items, conf):
@ -38,110 +38,6 @@ def find_files_recursively(items, conf):
yield item yield item
def supports_color():
supported_platform = not (platform.system() == 'Windows' and not
('ANSICON' in os.environ or
('TERM' in os.environ and
os.environ['TERM'] == 'ANSI')))
return (supported_platform and
hasattr(sys.stdout, 'isatty') and sys.stdout.isatty())
class Format(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 = ' %d:%d' % (problem.line, problem.column)
line += max(12 - len(line), 0) * ' '
line += problem.level
line += max(21 - len(line), 0) * ' '
line += problem.desc
if problem.rule:
line += ' (%s)' % problem.rule
return line
@staticmethod
def standard_color(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
@staticmethod
def github(problem, filename):
line = '::'
line += problem.level
line += ' file=' + filename + ','
line += 'line=' + format(problem.line) + ','
line += 'col=' + format(problem.column)
line += '::'
line += format(problem.line)
line += ':'
line += format(problem.column)
line += ' '
if problem.rule:
line += '[' + problem.rule + '] '
line += problem.desc
return line
def show_problems(problems, file, args_format, no_warn):
max_level = 0
first = True
if args_format == 'auto':
if ('GITHUB_ACTIONS' in os.environ and
'GITHUB_WORKFLOW' in os.environ):
args_format = 'github'
elif supports_color():
args_format = 'colored'
for problem in problems:
max_level = max(max_level, PROBLEM_LEVELS[problem.level])
if no_warn and (problem.level != 'error'):
continue
if args_format == 'parsable':
print(Format.parsable(problem, file))
elif args_format == 'github':
if first:
print('::group::%s' % file)
first = False
print(Format.github(problem, file))
elif args_format == 'colored':
if first:
print('\033[4m%s\033[0m' % file)
first = False
print(Format.standard_color(problem, file))
else:
if first:
print(file)
first = False
print(Format.standard(problem, file))
if not first and args_format == 'github':
print('::endgroup::')
if not first and args_format != 'parsable':
print('')
return max_level
def run(argv=None): def run(argv=None):
parser = argparse.ArgumentParser(prog=APP_NAME, parser = argparse.ArgumentParser(prog=APP_NAME,
description=APP_DESCRIPTION) description=APP_DESCRIPTION)
@ -159,8 +55,7 @@ def run(argv=None):
action='store', action='store',
help='custom configuration (as YAML source)') help='custom configuration (as YAML source)')
parser.add_argument('-f', '--format', parser.add_argument('-f', '--format',
choices=('parsable', 'standard', 'colored', 'github', choices=[*Formater.get_formaters_names(), 'auto'],
'auto'),
default='auto', help='format for parsing output') default='auto', help='format for parsing output')
parser.add_argument('-s', '--strict', parser.add_argument('-s', '--strict',
action='store_true', action='store_true',
@ -208,7 +103,8 @@ def run(argv=None):
if conf.locale is not None: if conf.locale is not None:
locale.setlocale(locale.LC_ALL, conf.locale) locale.setlocale(locale.LC_ALL, conf.locale)
max_level = 0 # problems dict: {file: problems}
all_problems = dict()
for file in find_files_recursively(args.files, conf): for file in find_files_recursively(args.files, conf):
filepath = file[2:] if file.startswith('./') else file filepath = file[2:] if file.startswith('./') else file
@ -218,20 +114,22 @@ def run(argv=None):
except EnvironmentError as e: except EnvironmentError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(-1) sys.exit(-1)
prob_level = show_problems(problems, file, args_format=args.format, all_problems[file] = [pb for pb in problems if pb]
no_warn=args.no_warnings)
max_level = max(max_level, prob_level)
# read yaml from stdin
if args.stdin: if args.stdin:
# read yaml from stdin
try: try:
problems = linter.run(sys.stdin, conf, '') problems = linter.run(sys.stdin, conf, '')
except EnvironmentError as e: except EnvironmentError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(-1) sys.exit(-1)
prob_level = show_problems(problems, 'stdin', args_format=args.format, all_problems['stdin'] = [pb for pb in problems if pb]
no_warn=args.no_warnings)
max_level = max(max_level, prob_level) max_level = show_all_problems(
all_problems,
args_format=args.format,
no_warn=args.no_warnings
)
if max_level == PROBLEM_LEVELS['error']: if max_level == PROBLEM_LEVELS['error']:
return_code = 1 return_code = 1

@ -0,0 +1,429 @@
from __future__ import print_function
import os
import platform
import sys
import json
import datetime
from yamllint.linter import PROBLEM_LEVELS
CODECLIMATE_SEVERITY = {
None: "info",
'warning': "minor",
'error': "major",
}
def supports_color():
supported_platform = not (platform.system() == 'Windows' and not
('ANSICON' in os.environ or
('TERM' in os.environ and
os.environ['TERM'] == 'ANSI')))
return (supported_platform and
hasattr(sys.stdout, 'isatty') and sys.stdout.isatty())
def run_on_gh():
"""Return if the currnet job is on github."""
return 'GITHUB_ACTIONS' in os.environ and 'GITHUB_WORKFLOW' in os.environ
def escape_xml(text):
"""Escape text for XML."""
text = text.replace('&', '&amp;')
text = text.replace('<', '&lt;')
text = text.replace('>', '&gt;')
text = text.replace('"', '&quot;')
text = text.replace("'", '&apos;')
return text
def severity_from_level(level):
if isinstance(level, int):
level = PROBLEM_LEVELS[level]
return CODECLIMATE_SEVERITY[level]
class Formater(object):
"""Any formater."""
# the formater name
name = ''
@classmethod
def get_formaters_names(cls):
"""Return all formaters names."""
return [f.name for f in cls.__subclasses__()]
@classmethod
def get_formater(cls, name, no_warn):
"""Return a formater instance."""
if name == 'auto':
if run_on_gh():
name = 'github'
elif supports_color():
name = 'colored'
else:
name = 'standard'
for formater in cls.__subclasses__():
if name == formater.name:
return formater(no_warn)
raise ValueError('unknown formater: %s' % name)
def __init__(self, no_warn):
"""Setup the formater."""
self.no_warn = no_warn
def show_problems_for_all_files(self, all_problems):
"""Show all problems of all files."""
raise NotImplementedError()
def show_problems_for_file(self, problems, file):
"""Show all problems of a specific file."""
raise NotImplementedError()
def show_problem(self, problem, file):
"""Show all problems of a specific file."""
raise NotImplementedError()
class ParsableFormater(Formater):
"""The parsable formater."""
name = 'parsable'
def show_problems_for_all_files(self, all_problems):
"""Show all problems of all files."""
string = ''
for file, problems in all_problems.items():
string += self.show_problems_for_file(problems, file)
return string
def show_problems_for_file(self, problems, file):
"""Show all problems of a specific file."""
string = ''
for problem in problems:
if problem.level is not None and (
problem.level == "error" or not self.no_warn
):
string += self.show_problem(problem, file)
return string
def show_problem(self, problem, file):
"""Show all problems of a specific file."""
if self.no_warn and (problem.level != 'error'):
return ''
return (
'%(file)s:%(line)s:%(column)s: [%(level)s] %(message)s\n' %
{
'file': file,
'line': problem.line,
'column': problem.column,
'level': problem.level,
'message': problem.message
}
)
class GithubFormater(Formater):
"""The github formater."""
name = 'github'
def show_problems_for_all_files(self, all_problems):
"""Show all problems of all files."""
string = ''
for file, problems in all_problems.items():
string += self.show_problems_for_file(problems, file)
return string
def show_problems_for_file(self, problems, file):
"""Show all problems of a specific file."""
string = '::group::%s\n' % file
for problem in problems:
if problem.level is not None and (
problem.level == "error" or not self.no_warn
):
string += self.show_problem(problem, file)
if string == '::group::%s\n' % file:
return ''
return string + '::endgroup::\n\n'
def show_problem(self, problem, file):
"""Show all problems of a specific file."""
if self.no_warn and (problem.level != 'error'):
return ''
line = '::'
line += problem.level
line += ' file=' + file + ','
line += 'line=' + format(problem.line) + ','
line += 'col=' + format(problem.column)
line += '::'
line += format(problem.line)
line += ':'
line += format(problem.column)
line += ' '
if problem.rule:
line += '[' + problem.rule + '] '
line += problem.desc
line += '\n'
return line
class ColoredFormater(Formater):
"""The colored formater."""
name = 'colored'
def show_problems_for_all_files(self, all_problems):
"""Show all problems of all files."""
string = ''
for file, problems in all_problems.items():
string += self.show_problems_for_file(problems, file)
return string
def show_problems_for_file(self, problems, file):
"""Show all problems of a specific file."""
string = '\033[4m%s\033[0m\n' % file
for problem in problems:
if problem.level is not None and (
problem.level == "error" or not self.no_warn
):
string += self.show_problem(problem, file)
if string == '\033[4m%s\033[0m\n' % file:
return ''
return string + '\n'
def show_problem(self, problem, file):
"""Show all problems of a specific file."""
if self.no_warn and (problem.level != 'error'):
return ''
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
line += '\n'
return line
class StandardFormater(Formater):
"""The standard formater."""
name = 'standard'
def show_problems_for_all_files(self, all_problems):
"""Show all problems of all files."""
string = ''
for file, problems in all_problems.items():
string += self.show_problems_for_file(problems, file)
return string
def show_problems_for_file(self, problems, file):
"""Show all problems of a specific file."""
string = file + '\n'
for problem in problems:
if problem.level is not None and (
problem.level == "error" or not self.no_warn
):
string += self.show_problem(problem, file)
if string == file + '\n':
return ''
return string + '\n'
def show_problem(self, problem, file):
"""Show all problems of a specific file."""
if self.no_warn and (problem.level != 'error'):
return ''
line = ' %d:%d' % (problem.line, problem.column)
line += max(12 - len(line), 0) * ' '
line += problem.level
line += max(21 - len(line), 0) * ' '
line += problem.desc
if problem.rule:
line += ' (%s)' % problem.rule
line += '\n'
return line
class JSONFormater(Formater):
"""The json formater."""
name = 'json'
def show_problems_for_all_files(self, all_problems):
"""Show all problems of all files."""
lst = []
for k, v in all_problems.items():
lst += self.show_problems_for_file(v, k)
return json.dumps(lst, indent=4) + '\n'
def show_problems_for_file(self, problems, file):
"""Show all problems of a specific file."""
lst = []
for problem in problems:
if problem.level is not None and (
problem.level == "error" or not self.no_warn
):
lst.append(self.show_problem(problem, file))
return lst
def show_problem(self, problem, file):
"""Show all problems of a specific file.
The desired format is:
>>> {
>>> "path": "dir/file.yaml",
>>> "line": 1337,
>>> "column": 42,
>>> "message": "duplication of key \"k\" in mapping",
>>> "rule": "key-duplicates",
>>> "level": "error"
>>> }
"""
dico = problem.dict
dico["message"] = dico.pop("desc")
dico["path"] = file
return dico
class JunitFormater(Formater):
"""The parsable formater."""
name = 'junitxml'
def show_problems_for_all_files(self, all_problems):
"""Show all problems of all files."""
string = '<?xml version="1.0" encoding="utf-8"?>\n<testsuites>\n'
errors = warnings = 0
lst = []
for k, v in all_problems.items():
lst += self.show_problems_for_file(v, k)
lines = []
for item in lst:
if item['level'] == 'warning':
warnings += 1
to_append = '<testcase classname="%s:%d:%d" name="%s" time="0.0"><failure message="%s"><\/failure><\/testcase>' # noqa
elif item['level'] == 'error':
errors += 1
to_append = '<testcase classname="%s:%d:%d" name="%s" time="0.0"><error message="%s"><\/error><\/testcase>' # noqa
lines.append(' ' * 8 + to_append % (
item['file'],
item['line'],
item['column'],
item['rule'],
escape_xml(item['desc']))
)
string += ' '*4 + '<testsuite name="yamllint" errors="%d" failures="%d" skipped="0" tests="%d" time="0" timestamp="%s" hostname="%s">\n' % (errors, warnings, errors + warnings, datetime.datetime.now().isoformat(), platform.node()) # noqa
string += '\n'.join(lines) + '\n'
string += ' </testsuite>\n</testsuites>\n'
return string
def show_problems_for_file(self, problems, file):
"""Show all problems of a specific file."""
lst = []
for problem in problems:
if problem.level is not None and (
problem.level == "error" or not self.no_warn
):
lst.append(self.show_problem(problem, file))
return lst
def show_problem(self, problem, file):
"""Show all problems of a specific file."""
return {**problem.dict, "file": file}
class CodeclimateFormater(Formater):
"""The codeclimate formater."""
name = 'codeclimate'
def show_problems_for_all_files(self, all_problems):
"""Show all problems of all files."""
lst = []
for k, v in all_problems.items():
lst += self.show_problems_for_file(v, k)
return json.dumps(lst, indent=4) + '\n'
def show_problems_for_file(self, problems, file):
"""Show all problems of a specific file."""
lst = []
for problem in problems:
if problem.level is not None and (
problem.level == "error" or not self.no_warn
):
lst.append(self.show_problem(problem, file))
return lst
def show_problem(self, problem, file):
"""Show all problems of a specific file.
Using the codeclimate format.
https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
* `type` -- **Required**. Must always be "issue".
* `check_name` -- **Required**. A unique name representing the static analysis check that emitted this issue.
* `description` -- **Required**. A string explaining the issue that was detected.
* `content` -- **Optional**. A markdown snippet describing the issue, including deeper explanations and links to other resources.
* `categories` -- **Required**. At least one category indicating the nature of the issue being reported.
* `location` -- **Required**. A `Location` object representing the place in the source code where the issue was discovered.
* `trace` -- **Optional.** A `Trace` object representing other interesting source code locations related to this issue.
* `remediation_points` -- **Optional**. An integer indicating a rough estimate of how long it would take to resolve the reported issue.
* `severity` -- **Optional**. A `Severity` string (`info`, `minor`, `major`, `critical`, or `blocker`) describing the potential impact of the issue found.
* `fingerprint` -- **Optional**. A unique, deterministic identifier for the specific issue being reported to allow a user to exclude it from future analyses.
For now the categories doc is empty, just put Style.
https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#categories
I don't find a value of remdiation_points, just set it at 1k.
I don't know how to calculate the fingerprint, maybe with a sha but it will be slow.
""" # noqa
return {
"type": "issue",
"check_name": problem.rule,
"description": problem.desc,
"content": problem.message,
"categories": ["Style"],
"location": {
"path": file,
"positions": {
"begin": {
"line": problem.line,
"column": problem.column
},
}
},
"remediation_points": 1_000,
"severity": severity_from_level(problem.level)
}
def max_level(all_problems):
"""Return the max level of all problems."""
all_levels = [
problem.level
for problems in all_problems.values()
for problem in problems
]
if all_levels:
return max(map(lambda x: int(PROBLEM_LEVELS[x]), all_levels))
return 0
def show_all_problems(all_problems, args_format, no_warn):
"""Print all problems, return the max level."""
fmt = Formater.get_formater(args_format, no_warn)
print(fmt.show_problems_for_all_files(all_problems), end='')
return max_level(all_problems)

@ -35,7 +35,14 @@ ENABLE_RULE_PATTERN = re.compile(r'^# yamllint enable( rule:\S+)*\s*$')
class LintProblem(object): class LintProblem(object):
"""Represents a linting problem found by yamllint.""" """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,
level=None
):
#: Line on which the problem was found (starting at 1) #: 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) #: Column on which the problem was found (starting at 1)
@ -44,7 +51,7 @@ class LintProblem(object):
self.desc = desc self.desc = desc
#: Identifier of the rule that detected the problem #: Identifier of the rule that detected the problem
self.rule = rule self.rule = rule
self.level = None self.level = level
@property @property
def message(self): def message(self):
@ -64,6 +71,18 @@ class LintProblem(object):
def __repr__(self): def __repr__(self):
return '%d:%d: %s' % (self.line, self.column, self.message) return '%d:%d: %s' % (self.line, self.column, self.message)
@property
def dict(self):
"""Return self as a dictionary."""
return {
"line": self.line,
"column": self.column,
"desc": self.desc,
"rule": self.rule,
"level": self.level,
"message": f"[{self.level}] {self.message}"
}
def get_cosmetic_problems(buffer, conf, filepath): def get_cosmetic_problems(buffer, conf, filepath):
rules = conf.enabled_rules(filepath) rules = conf.enabled_rules(filepath)

Loading…
Cancel
Save