danger-warnings 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ require 'warnings/plugin'
@@ -0,0 +1 @@
1
+ require 'warnings/gem_version'
@@ -0,0 +1,3 @@
1
+ module Warnings
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,31 @@
1
+ module Warnings
2
+ # An issue definition to be used for reports.
3
+ class Issue
4
+ # The name of the file this issue targets.
5
+ #
6
+ # @return [String]
7
+ attr_accessor :file_name
8
+ # The issue id the linter tool provides.
9
+ #
10
+ # @return [String]
11
+ attr_accessor :id
12
+ # The line this issue targets.
13
+ #
14
+ # @return [Integer]
15
+ attr_accessor :line
16
+ # The severity level of this issue.
17
+ # Possible values are `low` `medium` `high`.
18
+ #
19
+ # @return [Symbol]
20
+ attr_accessor :severity
21
+ # The text message describe this issue.
22
+ #
23
+ # @return [String]
24
+ attr_accessor :message
25
+
26
+ # The name of the issue id.
27
+ #
28
+ # @return [String]
29
+ attr_accessor :name
30
+ end
31
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'issue'
2
+
3
+ module Warnings
4
+ # Utility class to write the markdown report.
5
+ module MarkdownUtil
6
+ TABLE_HEADER = 'Severity|File|Message'.freeze
7
+ COLUMN_SEPARATOR = '|'.freeze
8
+ TABLE_SEPARATOR = "---#{COLUMN_SEPARATOR}---#{COLUMN_SEPARATOR}---".freeze
9
+ LINE_SEPARATOR = "\n".freeze
10
+
11
+ module_function
12
+
13
+ # Generate a markdown text message listing all issues as table.
14
+ #
15
+ # @param name [String] The name of the report to be printed.
16
+ # @param issues [Array<Issue>] List of parsed issues.
17
+ # @return [String] String in danger markdown format.
18
+ def generate(name, issues)
19
+ result = header_name(name)
20
+ result << header
21
+ result << issues(issues)
22
+ end
23
+
24
+ # Create the report name string.
25
+ #
26
+ # @param report_name [String] The name of the report.
27
+ # @return [String] Text containing header name of the report.
28
+ def header_name(report_name)
29
+ "# #{report_name}#{LINE_SEPARATOR}"
30
+ end
31
+
32
+ # Create the base table header line.
33
+ #
34
+ # @return [String] String containing a markdown table header line.
35
+ def header
36
+ result = TABLE_HEADER.dup
37
+ result << LINE_SEPARATOR
38
+ result << TABLE_SEPARATOR
39
+ result << LINE_SEPARATOR
40
+ end
41
+
42
+ # Create a string containing all issues prepared to be used in a markdown table.
43
+ #
44
+ # @param issues [Array<Issue>] List of parsed issues.
45
+ # @return [String] String containing all issues.
46
+ # rubocop:disable Metrics/AbcSize
47
+ def issues(issues)
48
+ result = ''
49
+ issues.each do |issue|
50
+ result << issue.severity.to_s.capitalize
51
+ result << COLUMN_SEPARATOR
52
+ result << "#{issue.file_name}:#{issue.line}"
53
+ result << COLUMN_SEPARATOR
54
+ result << "[#{issue.id}-#{issue.name}] #{issue.message}"
55
+ result << LINE_SEPARATOR
56
+ end
57
+ # rubocop:enable Metrics/AbcSize
58
+ result
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,45 @@
1
+ require_relative 'parser'
2
+ require_relative '../issue'
3
+
4
+ module Warnings
5
+ # Parser class for bandit generated json files.
6
+ class BanditParser < Parser
7
+ RESULTS_KEY = 'results'.freeze
8
+ FILE_TYPES = %i(json).freeze
9
+ NAME = 'Bandit'.freeze
10
+ ERROR_MISSING_KEY = "Missing bandit key '#{RESULTS_KEY}'.".freeze
11
+
12
+ def file_types
13
+ FILE_TYPES
14
+ end
15
+
16
+ def parse(file)
17
+ json_hash = json(file)
18
+ results_hash = json_hash[RESULTS_KEY]
19
+ raise(ERROR_MISSING_KEY) if results_hash.nil?
20
+
21
+ results_hash.each(&method(:store_issue))
22
+ end
23
+
24
+ def name
25
+ NAME
26
+ end
27
+
28
+ private
29
+
30
+ def store_issue(hash)
31
+ issue = Issue.new
32
+ issue.file_name = hash['filename']
33
+ issue.severity = to_severity(hash['issue_severity'])
34
+ issue.message = hash['issue_text']
35
+ issue.line = hash['line_number']
36
+ issue.id = hash['test_id']
37
+ issue.name = hash['test_name']
38
+ @issues << issue
39
+ end
40
+
41
+ def to_severity(severity)
42
+ severity.downcase.to_sym
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+ require 'abstract_method'
3
+
4
+ module Warnings
5
+ # Base parser class to define common methods.
6
+ class Parser
7
+ ERROR_FILE_NOT_EXIST = 'File \'%s\' does not exist.'.freeze
8
+ ERROR_EXT_NOT_SUPPORTED = 'File extension \'%s\' is not supported for parser %s.'.freeze
9
+ # All issues found by the parser.
10
+ #
11
+ # @return [Array<Issue>]
12
+ attr_accessor :issues
13
+ # Defines all supported file types for the parser.
14
+ #
15
+ # @return [Array<Symbol>] Array of file types.
16
+ abstract_method :file_types
17
+ # Execute the parser.
18
+ # Read the file and create an array of issues.
19
+ #
20
+ # @return [Array<Issue>] Array of issues.
21
+ abstract_method :parse
22
+ # Define a default name for the parser implementation.
23
+ #
24
+ # @return [String] Name of the parser implementation.
25
+ abstract_method :name
26
+
27
+ def initialize
28
+ @issues = []
29
+ end
30
+
31
+ protected
32
+
33
+ # Parse a file as json content.
34
+ #
35
+ # @param file_path [String] Path to a file to be read as json.
36
+ # @return [String] Hash of json values.
37
+ def json(file_path)
38
+ content = read_file(file_path)
39
+ JSON.parse(content)
40
+ end
41
+
42
+ private
43
+
44
+ # Evaluate and read the file into memory.
45
+ #
46
+ # @param file_path [String] Path to a file to be read.
47
+ # @raise If file does not exist or ist empty.
48
+ # @return [String] File content.
49
+ def read_file(file_path)
50
+ check_extname(file_path)
51
+ raise(format(ERROR_FILE_NOT_EXIST, file_path)) unless File.exist?(file_path)
52
+
53
+ File.read(file_path)
54
+ end
55
+
56
+ # Evaluate the files extension name.
57
+ #
58
+ # @param file_path [String] Path to a file to be evaluated.
59
+ # @raise If file extension is not supported by the current parser.
60
+ def check_extname(file_path)
61
+ ext = File.extname(file_path).delete('.')
62
+ raise(format(ERROR_EXT_NOT_SUPPORTED, ext, self.class.name)) unless file_types.include?(ext.to_sym)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'bandit_parser'
2
+
3
+ module Warnings
4
+ # Factory class for supported parsers.
5
+ class ParserFactory
6
+ ERROR_NOT_SUPPORTED = 'Parser \'%s\' not supported.'.freeze
7
+ AVAILABLE_PARSERS = {
8
+ bandit: BanditParser
9
+ }.freeze
10
+
11
+ # Create a new parser implementation.
12
+ #
13
+ # @param type [Symbol] A key symbol / name to identify the parser.
14
+ # @raise If no implementation could be found for the key.
15
+ # @return [Parser] Implementation
16
+ def self.create(type)
17
+ key = type
18
+ key = key.to_sym if key.respond_to?(:to_sym)
19
+ parser = AVAILABLE_PARSERS[key]
20
+ raise(format(ERROR_NOT_SUPPORTED, key)) if parser.nil?
21
+
22
+ parser.new
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,82 @@
1
+ require_relative 'reporter'
2
+
3
+ module Danger
4
+ # Create uniform issue reports for different parser types.
5
+ # @example Create a basic bandit report.
6
+ # warnings.report(
7
+ # name: 'Bandit Report',
8
+ # parser: :bandit,
9
+ # file: report/bandit.json
10
+ # )
11
+ #
12
+ # @example Create a bandit report and comment inline for all files.
13
+ # warnings.report(
14
+ # name: 'Bandit Report',
15
+ # parser: :bandit,
16
+ # file: report/bandit.json,
17
+ # inline: true,
18
+ # filter: false
19
+ # )
20
+ #
21
+ # @see Kyaak/danger-warnings
22
+ # @tags warnings, danger, parser, issues, report
23
+ class DangerWarnings < Plugin
24
+ # Whether to comment as markdown report or do an inline comment on the file.
25
+ #
26
+ # This will be set as default for all reporters used in this danger run.
27
+ # It can still be overridden by setting the value when using #report.
28
+ #
29
+ # @return [Bool] Use inline comments.
30
+ attr_accessor :inline
31
+ # Whether to filter and report only for changes files.
32
+ # If this is set to false, all issues are of a report are included in the comment.
33
+ #
34
+ # This will be set as default for all reporters used in this danger run.
35
+ # It can still be overridden by setting the value when using #report.
36
+ #
37
+ # @return [Bool] Filter for changes files.
38
+ attr_accessor :filter
39
+ # Whether to fail the PR if any high issue is reported.
40
+ #
41
+ # This will be set as default for all reporters used in this danger run.
42
+ # It can still be overridden by setting the value when using #report.
43
+ #
44
+ # @return [Bool] Fail on high issues.
45
+ attr_accessor :fail_error
46
+
47
+ def initialize(dangerfile)
48
+ super(dangerfile)
49
+ end
50
+
51
+ # Create a report for given arguments.
52
+ # name: 'Bandit Report',
53
+ # parser: :bandit,
54
+ # file: report/bandit.json,
55
+ # inline: true,
56
+ # filter: false
57
+ # @param args List of arguments to be used.
58
+ # @return [Reporter] The current reporter class which handles the issues.
59
+ def report(*args)
60
+ options = args.first
61
+ reporter = create_reporter(options)
62
+ reporter.report
63
+ reporter
64
+ end
65
+
66
+ private
67
+
68
+ # rubocop:disable Metrics/AbcSize
69
+ def create_reporter(options)
70
+ reporter = Warnings::Reporter.new(self)
71
+ reporter.parser = options[:parser]
72
+ reporter.name = options[:name]
73
+ reporter.file = options[:file]
74
+ reporter.baseline = options[:baseline]
75
+ reporter.inline = options[:inline] unless options[:inline].nil?
76
+ reporter.filter = options[:filter] unless options[:filter].nil?
77
+ reporter.fail_error = options[:fail_error] unless options[:fail_error].nil?
78
+ reporter
79
+ # rubocop:enable Metrics/AbcSize
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,158 @@
1
+ require_relative 'parser/parser_factory'
2
+ require_relative 'markdown_util'
3
+ require_relative 'severity'
4
+
5
+ module Warnings
6
+ # Base reporter class to define attributes and common method to create a report.
7
+ class Reporter
8
+ DEFAULT_INLINE = false
9
+ DEFAULT_FILTER = true
10
+ DEFAULT_FAIL = false
11
+ DEFAULT_NAME = 'Report'.freeze
12
+ ERROR_PARSER_NOT_SET = 'Parser is not set.'.freeze
13
+ ERROR_FILE_NOT_SET = 'File is not set.'.freeze
14
+ ERROR_HIGH_SEVERITY = '%s has high severity errors.'.freeze
15
+
16
+ # The name of this reporter. It is used to identify your report in the comments.
17
+ attr_writer :name
18
+ # Whether to comment a markdown report or do an inline comment on the file.
19
+ #
20
+ # @return [Bool] Use inline comments.
21
+ attr_accessor :inline
22
+ # Whether to filter and report only for changes files.
23
+ # If this is set to false, all issues are of a report are included in the comment.
24
+ #
25
+ # @return [Bool] Filter for changes files.
26
+ attr_accessor :filter
27
+ # Whether to fail the PR if any high issue is reported.
28
+ #
29
+ # @return [Bool] Fail on high issues.
30
+ attr_accessor :fail_error
31
+ # The parser to be used to read issues out of the file.
32
+ #
33
+ # @return [Symbol] Name of the parser.
34
+ attr_reader :parser
35
+ # The file path to parse.
36
+ #
37
+ # @return [String] Path to file.
38
+ attr_accessor :file
39
+ # Defines the baseline of file paths if needed.
40
+ # @example src/main/java
41
+ #
42
+ # @return [String] Path baseline for git files.
43
+ attr_accessor :baseline
44
+ # The generated implementation of the :parser.
45
+ #
46
+ # @return [Parser] Parser implementation
47
+ attr_reader :parser_impl
48
+ attr_reader :issues
49
+
50
+ def initialize(danger)
51
+ @danger = danger
52
+ @inline = DEFAULT_INLINE
53
+ @filter = DEFAULT_FILTER
54
+ @fail_error = DEFAULT_FAIL
55
+ @issues = []
56
+ end
57
+
58
+ # Start generating the report.
59
+ # Evaluate, parse and comment the found issues.
60
+ def report
61
+ validate
62
+ parse
63
+ filter_issues
64
+ comment
65
+ end
66
+
67
+ # Define the parser to be used.
68
+ #
69
+ # @@raise If no implementation can be found for the symbol.
70
+ # @param value [Symbol] A symbol key to match a parser implementation.
71
+ def parser=(value)
72
+ @parser = value
73
+ @parser_impl = ParserFactory.create(value)
74
+ end
75
+
76
+ # Return the name of this reporter.
77
+ # The name can have 3 values:
78
+ # - The name set using #name=
79
+ # - If name is not set, the name of the parser
80
+ # - If name and parser are not set, a DEFAULT_NAME
81
+ #
82
+ # @return [String] Name of the reporter.
83
+ def name
84
+ result = @name
85
+ result ||= "#{@parser_impl.name} #{DEFAULT_NAME}" if @parser_impl
86
+ result || DEFAULT_NAME
87
+ end
88
+
89
+ private
90
+
91
+ def filter_issues
92
+ return unless filter
93
+
94
+ git_files = @danger.git.modified_files + @danger.git.added_files
95
+ @issues.select! do |issue|
96
+ git_files.include?(issue_filename(issue))
97
+ end
98
+ end
99
+
100
+ def issue_filename(item)
101
+ result = ''
102
+ if baseline
103
+ result << baseline
104
+ result << '/' unless baseline.chars.last == '/'
105
+ end
106
+ result << item.file_name
107
+ end
108
+
109
+ def validate
110
+ raise ERROR_PARSER_NOT_SET if @parser_impl.nil?
111
+ raise ERROR_FILE_NOT_SET if @file.nil?
112
+ end
113
+
114
+ def parse
115
+ @parser_impl.parse(file)
116
+ @issues = @parser_impl.issues
117
+ end
118
+
119
+ def comment
120
+ return if @issues.empty?
121
+
122
+ inline ? inline_comment : markdown_comment
123
+ end
124
+
125
+ def inline_comment
126
+ @issues.each do |issue|
127
+ text = inline_text(issue)
128
+ if fail_error && high_issue?(issue)
129
+ @danger.fail(text, line: issue.line, file: issue.file_name)
130
+ else
131
+ @danger.warn(text, line: issue.line, file: issue.file_name)
132
+ end
133
+ end
134
+ end
135
+
136
+ def inline_text(issue)
137
+ "#{issue.severity.to_s.capitalize}\n[#{issue.id}-#{issue.name}]\n#{issue.message}"
138
+ end
139
+
140
+ def markdown_comment
141
+ text = MarkdownUtil.generate(name, @issues)
142
+ @danger.markdown(text)
143
+ @danger.fail(format(ERROR_HIGH_SEVERITY, name)) if fail_error && high_issues?
144
+ end
145
+
146
+ def high_issues?
147
+ result = false
148
+ @issues.each do |issue|
149
+ result = true if high_issue?(issue)
150
+ end
151
+ result
152
+ end
153
+
154
+ def high_issue?(issue)
155
+ issue.severity.eql?(:high)
156
+ end
157
+ end
158
+ end