Skip to content

Commit

Permalink
Add SARIF output (#459)
Browse files Browse the repository at this point in the history
Add option to output SARIF

Signed-off-by: Mathieu Rul <mathroule@gmail.com>
  • Loading branch information
mathroule committed Sep 6, 2023
1 parent 0268156 commit 01a3955
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 9 deletions.
14 changes: 9 additions & 5 deletions lib/mdl.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require_relative 'mdl/formatters/sarif'
require_relative 'mdl/cli'
require_relative 'mdl/config'
require_relative 'mdl/doc'
Expand Down Expand Up @@ -103,7 +104,7 @@ def self.run(argv = ARGV)
status = 1
error_lines.each do |line|
line += doc.offset # Correct line numbers for any yaml front matter
if Config[:json]
if Config[:json] || Config[:sarif]
results << {
'filename' => filename,
'line' => line,
Expand All @@ -118,12 +119,13 @@ def self.run(argv = ARGV)
end
end

# If we're not in JSON mode (URLs are in the object), and we cannot
# make real links (checking if we have a TTY is an OK heuristic for
# that) then, instead of making the output ugly with long URLs, we
# If we're not in JSON or SARIF mode (URLs are in the object), and we
# cannot make real links (checking if we have a TTY is an OK heuristic
# for that) then, instead of making the output ugly with long URLs, we
# print them at the end. And of course we only want to print each URL
# once.
if !Config[:json] && !$stdout.tty? && !docs_to_print.include?(rule)
if !Config[:json] && !Config[:sarif] &&
!$stdout.tty? && !docs_to_print.include?(rule)
docs_to_print << rule
end
end
Expand All @@ -132,6 +134,8 @@ def self.run(argv = ARGV)
if Config[:json]
require 'json'
puts JSON.generate(results)
elsif Config[:sarif]
puts SarifFormatter.generate(rules, results)
elsif docs_to_print.any?
puts "\nFurther documentation is available for these failures:"
docs_to_print.each do |rule|
Expand Down
6 changes: 6 additions & 0 deletions lib/mdl/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ class CLI
:description => 'JSON output',
:boolean => true

option :sarif,
:short => '-S',
:long => '--sarif',
:description => 'SARIF output',
:boolean => true

def run(argv = ARGV)
parse_options(argv)

Expand Down
89 changes: 89 additions & 0 deletions lib/mdl/formatters/sarif.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require 'json'

module MarkdownLint
# SARIF formatter
#
# @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
class SarifFormatter
class << self
def generate(rules, results)
matched_rules_id = results.map { |result| result['rule'] }.uniq
matched_rules = rules.select { |id, _| matched_rules_id.include?(id) }
JSON.generate(generate_sarif(matched_rules, results))
end

def generate_sarif(rules, results)
{
:'$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 => 'Markdown lint',
:version => MarkdownLint::VERSION,
:informationUri => 'https://github.com/markdownlint/markdownlint',
:rules => generate_sarif_rules(rules),
},
},
:results => generate_sarif_results(rules, results),
}
],
}
end

def generate_sarif_rules(rules)
rules.map do |id, rule|
{
:id => id,
:name => rule.aliases.first.split('-').map(&:capitalize).join,
:defaultConfiguration => {
:level => 'note',
},
:properties => {
:description => rule.description,
:tags => rule.tags,
:queryURI => rule.docs_url,
},
:shortDescription => {
:text => rule.description,
},
:fullDescription => {
:text => rule.description,
},
:helpUri => rule.docs_url,
:help => {
:text => "More info: #{rule.docs_url}",
:markdown => "[More info](#{rule.docs_url})",
},
}
end
end

def generate_sarif_results(rules, results)
results.map do |result|
{
:ruleId => result['rule'],
:ruleIndex => rules.find_index { |id, _| id == result['rule'] },
:message => {
:text => "#{result['rule']} - #{result['description']}",
},
:locations => [
{
:physicalLocation => {
:artifactLocation => {
:uri => result['filename'],
:uriBaseId => '%SRCROOT%',
},
:region => {
:startLine => result['line'],
},
},
}
],
}
end
end
end
end
end
1 change: 1 addition & 0 deletions test/fixtures/output/json/with_matches.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"filename":"(stdin)","line":1,"rule":"MD002","aliases":["first-header-h1"],"description":"First header should be a top level header","docs":"https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md002---first-header-should-be-a-top-level-header"}]
1 change: 1 addition & 0 deletions test/fixtures/output/json/without_matches.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
1 change: 1 addition & 0 deletions test/fixtures/output/sarif/with_matches.sarif
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$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":"Markdown lint","version":"0.12.0","informationUri":"https://github.com/markdownlint/markdownlint","rules":[{"id":"MD002","name":"FirstHeaderH1","defaultConfiguration":{"level":"note"},"properties":{"description":"First header should be a top level header","tags":["headers"],"queryURI":"https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md002---first-header-should-be-a-top-level-header"},"shortDescription":{"text":"First header should be a top level header"},"fullDescription":{"text":"First header should be a top level header"},"helpUri":"https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md002---first-header-should-be-a-top-level-header","help":{"text":"More info: https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md002---first-header-should-be-a-top-level-header","markdown":"[More info](https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md002---first-header-should-be-a-top-level-header)"}}]}},"results":[{"ruleId":"MD002","ruleIndex":0,"message":{"text":"MD002 - First header should be a top level header"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"(stdin)","uriBaseId":"%SRCROOT%"},"region":{"startLine":1}}}]}]}]}
1 change: 1 addition & 0 deletions test/fixtures/output/sarif/without_matches.sarif
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$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":"Markdown lint","version":"0.12.0","informationUri":"https://github.com/markdownlint/markdownlint","rules":[]}},"results":[]}]}
23 changes: 19 additions & 4 deletions test/test_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,31 @@ def test_help_text
def test_json_output
result = run_cli_with_input('-j', "# header\n")
assert_ran_ok(result)
assert_equal("[]\n", result[:stdout])
expected = File.read('test/fixtures/output/json/without_matches.json')
assert_equal(expected, result[:stdout])
end

def test_json_output_with_matches
result = run_cli_with_input('-j -r MD002', "## header2\n")
assert_equal(1, result[:status])
assert_equal('', result[:stderr])
d = JSON.parse(result[:stdout])
assert_match(d[0]['rule'], 'MD002')
assert_match(d[0]['docs'], 'https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md002---first-header-should-be-a-top-level-header')
expected = File.read('test/fixtures/output/json/with_matches.json')
assert_equal(expected, result[:stdout])
end

def test_sarif_output
result = run_cli_with_input('-S', "# header\n")
assert_ran_ok(result)
expected = File.read('test/fixtures/output/sarif/without_matches.sarif')
assert_equal(expected, result[:stdout])
end

def test_sarif_output_with_matches
result = run_cli_with_input('-S -r MD002', "## header2\n")
assert_equal(1, result[:status])
assert_equal('', result[:stderr])
expected = File.read('test/fixtures/output/sarif/with_matches.sarif')
assert_equal(expected, result[:stdout])
end

def test_default_ruleset_loading
Expand Down

0 comments on commit 01a3955

Please sign in to comment.