Merge ad46f33b1d
into b8c85f0dfd
commit
f461280cf7
@ -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(
|
||||
('&', '&'),
|
||||
('<', '<'),
|
||||
('>', '>'),
|
||||
('"', '"'),
|
||||
("'", '''),
|
||||
("too many blank lines (3 > 2)", 'too many blank lines (3 > 2)'),
|
||||
('line too long (100 > 80 characters)', 'line too long (100 > 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)
|
@ -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('&', '&')
|
||||
text = text.replace('<', '<')
|
||||
text = text.replace('>', '>')
|
||||
text = text.replace('"', '"')
|
||||
text = text.replace("'", ''')
|
||||
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)
|
Loading…
Reference in New Issue