diff --git a/pyproject.toml b/pyproject.toml
index 3d8f7d0..172b5ee 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,7 +45,7 @@ build-backend = "setuptools.build_meta"
requires = ["setuptools >= 61"]
[tool.setuptools]
-packages = ["yamllint", "yamllint.conf", "yamllint.rules"]
+packages = ["yamllint", "yamllint.conf", "yamllint.formatters", "yamllint.rules"]
[tool.setuptools.package-data]
yamllint = ["conf/*.yaml"]
diff --git a/tests/common.py b/tests/common.py
index 65af63b..9546c77 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -40,10 +40,7 @@ class RuleTestCase(unittest.TestCase):
for key in kwargs:
assert key.startswith('problem')
if len(kwargs[key]) > 2:
- if kwargs[key][2] == 'syntax':
- rule_id = None
- else:
- rule_id = kwargs[key][2]
+ rule_id = kwargs[key][2]
else:
rule_id = self.rule_id
expected_problems.append(linter.LintProblem(
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 419af92..26d828d 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -582,6 +582,94 @@ class CommandLineTestCase(unittest.TestCase):
self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (0, expected_out, ''))
+ def test_run_format_sarif(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'sarif'))
+ expected_out = (
+ '{"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif'
+ '-spec/master/Schemata/sarif-schema-2.1.0.json", "version": '
+ '"2.1.0", "runs": [{"tool": {"driver": {"name": "yamllint", '
+ '"version": "1.32.0", "informationUri": '
+ '"https://yamllint.readthedocs.io", "rules": [{"id": '
+ '"trailing-spaces", "name": "TrailingSpaces", '
+ '"defaultConfiguration": {"level": "error"}, "properties": {'
+ '"description": "trailing spaces", "tags": [], "queryUri": '
+ '"https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.trailing_spaces"}, "shortDescription": {"text": '
+ '"trailing spaces"}, "fullDescription": {"text": "trailing '
+ 'spaces"}, "helpUri": '
+ '"https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.trailing_spaces", "help": {"text": "More info: '
+ 'https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.trailing_spaces", "markdown": "[More info]('
+ 'https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.trailing_spaces)"}}, {"id": '
+ '"new-line-at-end-of-file", "name": "NewLineAtEndOfFile", '
+ '"defaultConfiguration": {"level": "error"}, "properties": {'
+ '"description": "no new line character at the end of file", '
+ '"tags": [], "queryUri": '
+ '"https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.new_line_at_end_of_file"}, "shortDescription": '
+ '{"text": "no new line character at the end of file"}, '
+ '"fullDescription": {"text": "no new line character at the end '
+ 'of file"}, "helpUri": '
+ '"https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.new_line_at_end_of_file", "help": {"text": '
+ '"More info: https://yamllint.readthedocs.io/en/v1.32.0/rules'
+ '.html#module-yamllint.rules.new_line_at_end_of_file", '
+ '"markdown": "[More info]('
+ 'https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.new_line_at_end_of_file)"}}]}}, "results": [{'
+ '"ruleId": "trailing-spaces", "ruleIndex": 0, "message": {'
+ '"text": "trailing spaces (trailing-spaces)"}, "locations": [{'
+ '"physicalLocation": {"artifactLocation": {"uri": "%s", '
+ '"uriBaseId": "%%SRCROOT%%"}, "region": {"startLine": 2, '
+ '"startColumn": 4}}}]}, {"ruleId": "new-line-at-end-of-file", '
+ '"ruleIndex": 1, "message": {"text": "no new line character at '
+ 'the end of file (new-line-at-end-of-file)"}, "locations": [{'
+ '"physicalLocation": {"artifactLocation": {"uri": "%s", '
+ '"uriBaseId": "%%SRCROOT%%"}, "region": {"startLine": 3, '
+ '"startColumn": 4}}}]}]}]}\n'
+ % (path, path))
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_format_sarif_warning(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'sarif'))
+ expected_out = (
+ '{"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif'
+ '-spec/master/Schemata/sarif-schema-2.1.0.json", "version": '
+ '"2.1.0", "runs": [{"tool": {"driver": {"name": "yamllint", '
+ '"version": "1.32.0", "informationUri": '
+ '"https://yamllint.readthedocs.io", "rules": [{"id": '
+ '"document-start", "name": "DocumentStart", '
+ '"defaultConfiguration": {"level": "warning"}, "properties": {'
+ '"description": "missing document start \\"---\\"", "tags": [], '
+ '"queryUri": "https://yamllint.readthedocs.io/en/v1.32.0/rules'
+ '.html#module-yamllint.rules.document_start"}, '
+ '"shortDescription": {"text": "missing document start '
+ '\\"---\\""}, "fullDescription": {"text": "missing document '
+ 'start \\"---\\""}, "helpUri": '
+ '"https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.document_start", "help": {"text": "More info: '
+ 'https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.document_start", "markdown": "[More info]('
+ 'https://yamllint.readthedocs.io/en/v1.32.0/rules.html#module'
+ '-yamllint.rules.document_start)"}}]}}, "results": [{"ruleId": '
+ '"document-start", "ruleIndex": 0, "message": {"text": "missing '
+ 'document start \\"---\\" (document-start)"}, "locations": [{'
+ '"physicalLocation": {"artifactLocation": {"uri": "%s", '
+ '"uriBaseId": "%%SRCROOT%%"}, "region": {"startLine": 1, '
+ '"startColumn": 1}}}]}]}]}\n'
+ % path)
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (0, expected_out, ''))
+
def test_run_format_github(self):
path = os.path.join(self.wd, 'a.yaml')
diff --git a/tests/test_syntax_errors.py b/tests/test_syntax_errors.py
index 507ab5a..ece4474 100644
--- a/tests/test_syntax_errors.py
+++ b/tests/test_syntax_errors.py
@@ -17,7 +17,7 @@ from tests.common import RuleTestCase
class YamlLintTestCase(RuleTestCase):
- rule_id = None # syntax error
+ rule_id = 'syntax' # syntax error
def test_syntax_errors(self):
self.check('---\n'
diff --git a/yamllint/cli.py b/yamllint/cli.py
index d7fa156..0882a6e 100644
--- a/yamllint/cli.py
+++ b/yamllint/cli.py
@@ -22,6 +22,7 @@ import sys
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
from yamllint import linter
from yamllint.config import YamlLintConfig, YamlLintConfigError
+from yamllint.formatters import colored, github, parsable, sarif, standard
from yamllint.linter import PROBLEM_LEVELS
@@ -46,63 +47,7 @@ def supports_color():
hasattr(sys.stdout, 'isatty') and sys.stdout.isatty())
-class Format:
- @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
-
+def show_results(results, args_format, no_warn):
if args_format == 'auto':
if ('GITHUB_ACTIONS' in os.environ and
'GITHUB_WORKFLOW' in os.environ):
@@ -110,35 +55,16 @@ def show_problems(problems, file, args_format, no_warn):
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
+ if args_format == 'parsable':
+ return parsable.format_results(results, no_warn)
+ elif args_format == 'github':
+ return github.format_results(results, no_warn)
+ elif args_format == 'sarif':
+ return sarif.format_results(results, no_warn)
+ elif args_format == 'colored':
+ return colored.format_results(results, no_warn)
+ else:
+ return standard.format_results(results, no_warn)
def find_project_config_filepath(path='.'):
@@ -174,7 +100,7 @@ def run(argv=None):
help='list files to lint and exit')
parser.add_argument('-f', '--format',
choices=('parsable', 'standard', 'colored', 'github',
- 'auto'),
+ 'sarif', 'auto'),
default='auto', help='format for parsing output')
parser.add_argument('-s', '--strict',
action='store_true',
@@ -225,8 +151,7 @@ def run(argv=None):
print(file)
sys.exit(0)
- max_level = 0
-
+ results = {}
for file in find_files_recursively(args.files, conf):
filepath = file[2:] if file.startswith('./') else file
try:
@@ -235,9 +160,7 @@ def run(argv=None):
except OSError as e:
print(e, file=sys.stderr)
sys.exit(-1)
- prob_level = show_problems(problems, file, args_format=args.format,
- no_warn=args.no_warnings)
- max_level = max(max_level, prob_level)
+ results[filepath] = problems
# read yaml from stdin
if args.stdin:
@@ -246,9 +169,10 @@ def run(argv=None):
except OSError as e:
print(e, file=sys.stderr)
sys.exit(-1)
- prob_level = show_problems(problems, 'stdin', args_format=args.format,
- no_warn=args.no_warnings)
- max_level = max(max_level, prob_level)
+ results['stdin'] = problems
+
+ max_level = show_results(results, args_format=args.format,
+ no_warn=args.no_warnings)
if max_level == PROBLEM_LEVELS['error']:
return_code = 1
diff --git a/yamllint/formatters/colored.py b/yamllint/formatters/colored.py
new file mode 100644
index 0000000..e3e5777
--- /dev/null
+++ b/yamllint/formatters/colored.py
@@ -0,0 +1,48 @@
+# 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 .
+
+from yamllint.linter import PROBLEM_LEVELS
+
+
+def format_results(results, no_warn):
+ max_level = 0
+
+ for file in results:
+ print('\033[4m%s\033[0m' % file)
+
+ for problem in results[file]:
+ max_level = max(max_level, PROBLEM_LEVELS[problem.level])
+ if no_warn and (problem.level != 'error'):
+ continue
+
+ print(format_problem(problem))
+
+ print('')
+
+ return max_level
+
+
+def format_problem(problem):
+ 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
diff --git a/yamllint/formatters/github.py b/yamllint/formatters/github.py
new file mode 100644
index 0000000..f2dbe3c
--- /dev/null
+++ b/yamllint/formatters/github.py
@@ -0,0 +1,52 @@
+# 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 .
+
+from yamllint.linter import PROBLEM_LEVELS
+
+
+def format_results(results, no_warn):
+ max_level = 0
+
+ for file in results:
+ print('::group::%s' % file)
+
+ for problem in results[file]:
+ max_level = max(max_level, PROBLEM_LEVELS[problem.level])
+ if no_warn and (problem.level != 'error'):
+ continue
+
+ print(format_problem(problem, file))
+
+ print('::endgroup::')
+ print('')
+
+ return max_level
+
+
+def format_problem(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
diff --git a/yamllint/formatters/parsable.py b/yamllint/formatters/parsable.py
new file mode 100644
index 0000000..eb32462
--- /dev/null
+++ b/yamllint/formatters/parsable.py
@@ -0,0 +1,39 @@
+# 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 .
+
+from yamllint.linter import PROBLEM_LEVELS
+
+
+def format_results(results, no_warn):
+ max_level = 0
+
+ for file in results:
+ for problem in results[file]:
+ max_level = max(max_level, PROBLEM_LEVELS[problem.level])
+ if no_warn and (problem.level != 'error'):
+ continue
+
+ print(format_problem(problem, file))
+
+ return max_level
+
+
+def format_problem(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})
diff --git a/yamllint/formatters/sarif.py b/yamllint/formatters/sarif.py
new file mode 100644
index 0000000..9980be4
--- /dev/null
+++ b/yamllint/formatters/sarif.py
@@ -0,0 +1,120 @@
+# 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 .
+
+import json
+
+from yamllint import APP_VERSION
+from yamllint.linter import PROBLEM_LEVELS
+
+
+def format_results(results, no_warn):
+ max_level = 0
+
+ sarif = {
+ '$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec'
+ '/master/Schemata/sarif-schema-2.1.0.json',
+ 'version': '2.1.0',
+ 'runs': [
+ {
+ 'tool': {
+ 'driver': {
+ 'name': 'yamllint',
+ 'version': APP_VERSION,
+ 'informationUri': 'https://yamllint.readthedocs.io',
+ 'rules': []
+ },
+ },
+ 'results': []
+ }
+ ]
+ }
+
+ rules = {}
+ max_rule_index = 0
+
+ for file in results:
+ for problem in results[file]:
+ max_level = max(max_level, PROBLEM_LEVELS[problem.level])
+
+ if problem.rule in rules:
+ rule_index = rules[problem.rule]
+ else:
+ rule_index = max_rule_index
+ rules[problem.rule] = max_rule_index
+ sarif['runs'][0]['tool']['driver']['rules'].append(format_rule(
+ problem))
+ max_rule_index += 1
+
+ sarif['runs'][0]['results'].append(format_result(rule_index,
+ problem, file))
+
+ print(json.dumps(sarif))
+
+ return max_level
+
+
+def format_rule(problem):
+ uri = 'https://yamllint.readthedocs.io/en/v%s/rules.html#module-yamllint' \
+ '.rules.%s' % (APP_VERSION, problem.rule.replace('-', '_'))
+
+ name = ''.join([word.capitalize() for word in problem.rule.split('-')])
+
+ return {
+ 'id': problem.rule,
+ 'name': name,
+ 'defaultConfiguration': {
+ 'level': problem.level
+ },
+ 'properties': {
+ 'description': problem.desc,
+ 'tags': [],
+ 'queryUri': uri,
+ },
+ 'shortDescription': {
+ 'text': problem.desc
+ },
+ 'fullDescription': {
+ 'text': problem.desc
+ },
+ 'helpUri': uri,
+ 'help': {
+ 'text': 'More info: {}'.format(uri),
+ 'markdown': '[More info]({})'.format(uri)
+ }
+ }
+
+
+def format_result(rule_index, problem, filename):
+ return {
+ 'ruleId': problem.rule,
+ 'ruleIndex': rule_index,
+ 'message': {
+ 'text': problem.message
+ },
+ 'locations': [
+ {
+ 'physicalLocation': {
+ 'artifactLocation': {
+ 'uri': filename,
+ 'uriBaseId': '%SRCROOT%'
+ },
+ 'region': {
+ 'startLine': problem.line,
+ 'startColumn': problem.column
+ }
+ }
+ }
+ ]
+ }
diff --git a/yamllint/formatters/standard.py b/yamllint/formatters/standard.py
new file mode 100644
index 0000000..7b256eb
--- /dev/null
+++ b/yamllint/formatters/standard.py
@@ -0,0 +1,45 @@
+# 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 .
+
+from yamllint.linter import PROBLEM_LEVELS
+
+
+def format_results(results, no_warn):
+ max_level = 0
+
+ for file in results:
+ print(file)
+
+ for problem in results[file]:
+ max_level = max(max_level, PROBLEM_LEVELS[problem.level])
+ if no_warn and (problem.level != 'error'):
+ continue
+
+ print(format_problem(problem))
+
+ print('')
+
+ return max_level
+
+
+def format_problem(problem):
+ 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
diff --git a/yamllint/linter.py b/yamllint/linter.py
index 5501bb5..3dc7888 100644
--- a/yamllint/linter.py
+++ b/yamllint/linter.py
@@ -180,7 +180,8 @@ def get_syntax_error(buffer):
except yaml.error.MarkedYAMLError as e:
problem = LintProblem(e.problem_mark.line + 1,
e.problem_mark.column + 1,
- 'syntax error: ' + e.problem + ' (syntax)')
+ 'syntax error: ' + e.problem,
+ 'syntax')
problem.level = 'error'
return problem