danger-wcc 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +46 -0
  3. data/.gitignore +6 -0
  4. data/.rubocop.yml +219 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +12 -0
  7. data/Dangerfile +5 -0
  8. data/Gemfile +6 -0
  9. data/Guardfile +35 -0
  10. data/LICENSE +201 -0
  11. data/README.md +2 -0
  12. data/Rakefile +25 -0
  13. data/danger-wcc.gemspec +58 -0
  14. data/lib/danger_plugin.rb +3 -0
  15. data/lib/version.rb +5 -0
  16. data/lib/wcc/commit_lint.rb +158 -0
  17. data/lib/wcc/commit_lint/README.md +3 -0
  18. data/lib/wcc/commit_lint/commit_check.rb +19 -0
  19. data/lib/wcc/commit_lint/empty_line_check.rb +22 -0
  20. data/lib/wcc/commit_lint/subject_cap_check.rb +22 -0
  21. data/lib/wcc/commit_lint/subject_length_check.rb +28 -0
  22. data/lib/wcc/commit_lint/subject_period_check.rb +22 -0
  23. data/lib/wcc/commit_lint/subject_words_check.rb +22 -0
  24. data/lib/wcc/default.jshintrc +5 -0
  25. data/lib/wcc/defaults.reek +131 -0
  26. data/lib/wcc/github.rb +24 -0
  27. data/lib/wcc/jshint.rb +63 -0
  28. data/lib/wcc/plugin.rb +128 -0
  29. data/lib/wcc/reek.rb +56 -0
  30. data/lib/wcc/rubocop_exceptions.rb +99 -0
  31. data/lib/wcc/todos.rb +78 -0
  32. data/lib/wcc/utils.rb +136 -0
  33. data/spec/fixtures/brakeman/a.tmp +13 -0
  34. data/spec/fixtures/brakeman/b.tmp +14 -0
  35. data/spec/fixtures/brakeman/brakeman.diff +20 -0
  36. data/spec/fixtures/brakeman/brakeman.out +14 -0
  37. data/spec/fixtures/exception_context.diff +15 -0
  38. data/spec/fixtures/exception_insert_context.diff +14 -0
  39. data/spec/fixtures/exception_misspelled.diff +14 -0
  40. data/spec/fixtures/exception_multiline_context.diff +20 -0
  41. data/spec/fixtures/exception_reenabled.diff +13 -0
  42. data/spec/fixtures/find_in_diff.rb +21 -0
  43. data/spec/fixtures/find_in_diff_2_chunks.diff +24 -0
  44. data/spec/fixtures/flay.diff +17 -0
  45. data/spec/fixtures/flay.txt +18 -0
  46. data/spec/fixtures/github/labels.json +72 -0
  47. data/spec/fixtures/github_pr.json +325 -0
  48. data/spec/fixtures/jshint/a.tmp +5 -0
  49. data/spec/fixtures/jshint/b.tmp +7 -0
  50. data/spec/fixtures/jshint/jshint.diff +13 -0
  51. data/spec/fixtures/jshint/out.jshint +7 -0
  52. data/spec/fixtures/no_exception.diff +10 -0
  53. data/spec/fixtures/no_todo.diff +13 -0
  54. data/spec/fixtures/reek/line_numbers.reek +121 -0
  55. data/spec/fixtures/reek/reek.diff +50 -0
  56. data/spec/fixtures/rubocop_exception.rb +39 -0
  57. data/spec/fixtures/todo.rb +21 -0
  58. data/spec/fixtures/todo_link_next_line.diff +14 -0
  59. data/spec/fixtures/todo_link_same_line.diff +13 -0
  60. data/spec/fixtures/todo_no_link.diff +13 -0
  61. data/spec/fixtures/todo_removed.diff +13 -0
  62. data/spec/fixtures_helper.rb +19 -0
  63. data/spec/spec_helper.rb +73 -0
  64. data/spec/wcc/commit_lint_spec.rb +392 -0
  65. data/spec/wcc/github_spec.rb +67 -0
  66. data/spec/wcc/jshint_spec.rb +68 -0
  67. data/spec/wcc/plugin_spec.rb +134 -0
  68. data/spec/wcc/reek_spec.rb +71 -0
  69. data/spec/wcc/rubocop_exceptions_spec.rb +136 -0
  70. data/spec/wcc/todos_spec.rb +96 -0
  71. data/spec/wcc/utils_spec.rb +134 -0
  72. data/spec/wcc_spec.rb +21 -0
  73. metadata +393 -0
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Github
4
+ def labels
5
+ @github_labels ||=
6
+ github.api.labels(repo_name)
7
+ end
8
+
9
+ def repo_name
10
+ github.pr_json['base']['repo']['full_name']
11
+ end
12
+
13
+ def pr_number
14
+ github.pr_json['number']
15
+ end
16
+
17
+ def add_labels(*new_labels)
18
+ existing = labels.map(&:name)
19
+ new_labels.reject { |l| existing.include?(l) }.each do |to_create|
20
+ github.api.add_label(repo_name, to_create)
21
+ end
22
+ github.api.add_labels_to_an_issue(repo_name, pr_number, new_labels)
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+
5
+ class Danger::DangerWCC < Danger::Plugin
6
+ class Jshint
7
+ include Utils
8
+
9
+ DEFAULT_OPTIONS = {
10
+ directory: 'app/assets/javascripts'
11
+ }.freeze
12
+
13
+ def initialize(plugin, options = {})
14
+ @plugin = plugin
15
+ @options = DEFAULT_OPTIONS.merge(options)
16
+ end
17
+
18
+ def perform
19
+ return unless Dir.exist?(@options[:directory])
20
+
21
+ run('PREFIX=/tmp npm install -g jshint')
22
+ diff = run_jshint_diff
23
+
24
+ # run jshint again to get line numbers
25
+ jshint_lines = run '/tmp/lib/node_modules/jshint/bin/jshint '\
26
+ "#{@options[:directory]}"
27
+ jshint_lines = jshint_lines.lines
28
+
29
+ each_addition_in_diff(diff) do |line|
30
+ add_jshint_warning(jshint_lines[line.line_number.right - 1])
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def run_jshint_diff
37
+ diff =
38
+ run_and_diff do
39
+ write_jshintrc
40
+ run('/tmp/lib/node_modules/jshint/bin/jshint '\
41
+ "#{@options[:directory]} | sed 's/line [0-9]*, col [0-9]*,//g'")
42
+ end
43
+ GitDiff.from_string(diff)
44
+ end
45
+
46
+ def write_jshintrc
47
+ return if File.exist?('.jshintrc')
48
+
49
+ default = File.join(File.dirname(__FILE__), 'default.jshintrc')
50
+ FileUtils.cp(default, './.jshintrc')
51
+ end
52
+
53
+ def add_jshint_warning(with_line_number)
54
+ return unless warning = with_line_number.match(
55
+ /^([^\:]+)\:\s+line\s+(\d+),\s+col\s+\d+,\s+(.+)$/i
56
+ )
57
+
58
+ plugin.warn(warning.captures[2],
59
+ file: warning.captures[0],
60
+ line: warning.captures[1].to_i)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+ require_relative 'github'
5
+ require_relative 'todos'
6
+ require_relative 'rubocop_exceptions'
7
+ require_relative 'commit_lint'
8
+ require_relative 'reek'
9
+ require_relative 'jshint'
10
+
11
+ class Danger::DangerWCC < Danger::Plugin
12
+ include Utils
13
+ include Github
14
+
15
+ CHECKS = %i[
16
+ rubocop_exceptions
17
+ todos
18
+ commit_lint
19
+ reek
20
+ flay
21
+ brakeman
22
+ jshint
23
+ ].freeze
24
+
25
+ # Runs all the included checks in the plugin
26
+ def all(options = {})
27
+ to_run = CHECKS.reject { |check_name| options[check_name] == false }
28
+ raise ArgumentError, 'No Enabled Checks' if to_run.empty?
29
+
30
+ to_run.each do |check_name|
31
+ public_send(check_name, options.fetch(check_name, {}))
32
+ end
33
+ end
34
+
35
+ # Checks for added TODOs
36
+ def todos(options = {})
37
+ logger.info "TODOs: #{options}"
38
+ Todos.new(self, options).perform
39
+ end
40
+
41
+ # Checks for if Rubocop was disabled in the source
42
+ def rubocop_exceptions(options = {})
43
+ logger.info "rubocop_exceptions: #{options}"
44
+ RubocopExceptions.new(self, options).perform
45
+ end
46
+
47
+ # Lints the commit messages
48
+ def commit_lint(options = {})
49
+ logger.info "commit_lint: #{options}"
50
+ CommitLint.new(self, DEFAULT_COMMIT_LINT_OPTIONS.merge(options)).perform
51
+ end
52
+
53
+ def reek(options = {})
54
+ logger.info "reek: #{options}"
55
+ Reek.new(self, options).perform
56
+ end
57
+
58
+ def brakeman(options = {})
59
+ logger.info "brakeman: #{options}"
60
+ diff = run_and_diff('bundle exec brakeman -f tabs 2>/dev/null '\
61
+ '| sed s@`pwd`/@@ | sed -E "s/([0-9]+)//g"')
62
+ diff = GitDiff.from_string(diff)
63
+
64
+ brakeman_lines = run 'bundle exec brakeman -f tabs 2>/dev/null '\
65
+ '| sed s@`pwd`/@@'
66
+ brakeman_lines = brakeman_lines.lines
67
+
68
+ each_addition_in_diff(diff) do |line|
69
+ fields = brakeman_lines[line.line_number.right - 1].split("\t")
70
+
71
+ add_brakeman_error(fields, fields[0])
72
+ end
73
+ end
74
+
75
+ def flay(options = {})
76
+ logger.info "flay: #{options}"
77
+ flay_results = parse_flay_results
78
+
79
+ each_addition_in_diff do |add, _h, file|
80
+ search = "#{file.b_path}:#{add.line_number.right}"
81
+ flay_results.each do |warning, lines|
82
+ other = lines.reject { |l| l == search }
83
+ next unless other.length < lines.length
84
+
85
+ add_flay_warning(warning, other, file, add)
86
+ end
87
+ end
88
+ end
89
+
90
+ def jshint(options = {})
91
+ logger.info "jshint: #{options}"
92
+ Jshint.new(self, options).perform
93
+ end
94
+
95
+ private
96
+
97
+ def parse_flay_results
98
+ flay = run('bundle exec flay app/**/*.rb lib/**/*.rb')
99
+ flay_results = {}
100
+ current = nil
101
+ flay.lines.each do |flay_line|
102
+ if m = flay_line.match(/^\d+\)\s+(.+)$/i)
103
+ current = []
104
+ flay_results[m.captures[0]] = current
105
+ elsif current && flay_line =~ /^\s+.+\:\d+\s*$/i
106
+ current << flay_line.strip!
107
+ end
108
+ end
109
+ flay_results
110
+ end
111
+
112
+ def add_brakeman_error(fields, file)
113
+ message = "#{fields[2]} #{fields[3]} #{fields[4]} confidence: #{fields[5]}"
114
+ fail(message, file: file, line: fields[1].to_i)
115
+ end
116
+
117
+ def add_flay_warning(warning, other, file, line)
118
+ other = other.map { |l| github.html_link(l.gsub(/\:/, '#L')) }
119
+ message = "#{warning} at:\n " + other.join("\n ")
120
+ warn(message, file: file.b_path, line: line.line_number.right)
121
+ end
122
+
123
+ DEFAULT_COMMIT_LINT_OPTIONS = {
124
+ warn: :all,
125
+ disable: %i[subject_period subject_cap],
126
+ subject_length: { max: 72 }
127
+ }.freeze
128
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+
5
+ class Danger::DangerWCC < Danger::Plugin
6
+ class Reek
7
+ include Utils
8
+
9
+ def initialize(plugin, options = {})
10
+ @plugin = plugin
11
+ @options = options
12
+ end
13
+
14
+ def perform
15
+ # get the diff of new reek issues
16
+ diff = run_reek_diff
17
+
18
+ # run reek again to get line numbers
19
+ reek_lines = run 'bundle exec reek --single-line --no-progress --no-color'
20
+ reek_lines = reek_lines.lines
21
+
22
+ each_addition_in_diff(diff) do |line|
23
+ add_reek_warning(line, reek_lines[line.line_number.right - 1])
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def run_reek_diff
30
+ diff =
31
+ run_and_diff do
32
+ write_reek_config
33
+ run('bundle exec reek --single-line --no-progress '\
34
+ '--no-color --no-line-numbers')
35
+ end
36
+ GitDiff.from_string(diff)
37
+ end
38
+
39
+ def write_reek_config
40
+ return if File.exist?('.reek')
41
+ return unless Dir.glob('*.reek').empty?
42
+
43
+ default = File.join(File.dirname(__FILE__), 'defaults.reek')
44
+ FileUtils.cp(default, './')
45
+ end
46
+
47
+ def add_reek_warning(line, with_line_number)
48
+ return unless warning = line.content.match(/^\+?\s*([^\:]+\:\s+.+)$/i)
49
+
50
+ line_info = with_line_number.match(/^\s*([^\:]+)\:(\d+)\:/i)
51
+ plugin.warn(format_links_as_markdown(warning.captures[0]),
52
+ file: line_info.captures[0],
53
+ line: line_info.captures[1].to_i)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+
5
+ class Danger::DangerWCC < Danger::Plugin
6
+ class RubocopExceptions
7
+ include Utils
8
+
9
+ DISABLE_REGEX = /^(?:\+\s)?.*\#\s*rubocop\:disable\s+(\S+)/i
10
+ ENABLE_REGEX = /^(?:\+\s)?\s*\#\s*rubocop\:enable\s+(\S+)/i
11
+ COMMENT_LINE_REGEX = /^(?:\+\s)?\s*\#\s*(.+)$/i
12
+
13
+ def initialize(plugin, options = {})
14
+ @plugin = plugin
15
+ @options = options
16
+ end
17
+
18
+ def perform
19
+ find_new_exceptions.each do |e|
20
+ message = build_message(e)
21
+ severity = message_severity(e)
22
+ issue(message,
23
+ severity: severity, file: e[:file], line: e[:disabled_at])
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def build_message(e)
30
+ message = "Rubocop rule #{e[:rule]} disabled in"\
31
+ " #{plugin.github.html_link(e[:file])}"
32
+ message +=
33
+ if !e[:context_lines].empty?
34
+ " explanation:\n\t> " + e[:context_lines].join("\n\t> ")
35
+ else
36
+ " \nPlease provide an explanation why this rule was disabled."
37
+ end
38
+ unless e[:reenabled]
39
+ message += "\n\nThe rule was not reenabled!\n"\
40
+ 'Please add a `rubocop:enable` comment so the rule is disabled '\
41
+ 'for the minimal scope.'
42
+ end
43
+ message
44
+ end
45
+
46
+ def message_severity(e)
47
+ if !e[:reenabled]
48
+ 'fail'
49
+ elsif e[:context_lines].empty?
50
+ 'warn'
51
+ else
52
+ 'message'
53
+ end
54
+ end
55
+
56
+ def find_new_exceptions
57
+ find_in_diff(DISABLE_REGEX) do |m, line, hunk, file, _diff|
58
+ make_violation(File.read(file.b_path), hunk, line, m.captures[0])
59
+ .merge!(file: file.b_path)
60
+ end
61
+ end
62
+
63
+ def make_violation(file_contents, hunk, line, rule)
64
+ reenable_line_offset = find_reenable(file_contents,
65
+ line.line_number.right,
66
+ rule)
67
+
68
+ {
69
+ rule: rule,
70
+ disabled_at: line.line_number.right,
71
+ reenabled: !reenable_line_offset.nil?,
72
+ context_lines: find_context(hunk.lines.drop(hunk.lines.index(line)))
73
+ }
74
+ end
75
+
76
+ def find_context(diff_lines)
77
+ # search for all non-`rubocop:` comment lines immediately
78
+ # preceding this disable which were added by this hunk
79
+ diff_lines
80
+ .drop_while { |l| rubocop_line?(l) }
81
+ .take_while { |l| l.addition? && l.content =~ COMMENT_LINE_REGEX }
82
+ .map { |l| l.content.match(COMMENT_LINE_REGEX).captures[0] }
83
+ end
84
+
85
+ def find_reenable(file_contents, line_number, rule)
86
+ # search for the reenabling of this line
87
+ length_to_end = file_contents.lines.length - line_number
88
+ file_contents.lines[line_number, length_to_end]
89
+ .find_index do |l|
90
+ m = l.match(ENABLE_REGEX)
91
+ !m.nil? && rule.casecmp(m.captures[0]) == 0
92
+ end
93
+ end
94
+
95
+ def rubocop_line?(l)
96
+ l.content =~ DISABLE_REGEX || l.content =~ ENABLE_REGEX
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+
5
+ class Danger::DangerWCC < Danger::Plugin
6
+ class Todos
7
+ include Utils
8
+
9
+ TODO_REGEX = /^\+.*\#.*TODO:?/i
10
+ LINK_REGEX = /\#.*(https?\:\/\/\S+)/i
11
+
12
+ def initialize(plugin, options = {})
13
+ @plugin = plugin
14
+ @options = options
15
+ end
16
+
17
+ def perform
18
+ find_new_todos.each do |result|
19
+ issue_message(result)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def issue_message(result)
26
+ if result[:link]
27
+ plugin.message "TODO added in #{result[:file_link]} "\
28
+ "referencing [#{result[:link]}](#{result[:link]})",
29
+ file: result[:file], line: result[:line]
30
+ else
31
+ plugin.warn "TODO added in #{result[:file_link]} - "\
32
+ 'is there a card associated with that?',
33
+ file: result[:file], line: result[:line]
34
+ end
35
+ end
36
+
37
+ def find_new_todos
38
+ find_in_diff(TODO_REGEX) do |_m, line, _hunk, file, _diff|
39
+ make_violation(file, line)
40
+ end
41
+ end
42
+
43
+ def make_violation(file, todo_line)
44
+ link_line_number, link = find_link_in_context(file, todo_line)
45
+
46
+ {
47
+ link: link,
48
+ file: file.b_path,
49
+ file_link: @plugin.github.html_link(file.b_path),
50
+ line: max(link_line_number, todo_line.line_number.right),
51
+ warn: link.nil?
52
+ }
53
+ end
54
+
55
+ def grab_context_lines(file, line_number)
56
+ contents = File.read(file.b_path)
57
+ # grab the line and the lines immediately before and after it
58
+ contents.lines[max(line_number - 2, 0)..line_number]
59
+ end
60
+
61
+ def find_link_in_context(file, line)
62
+ context = grab_context_lines(file, line.line_number.right)
63
+ context.each_with_index do |l, i|
64
+ if m = l.match(LINK_REGEX)
65
+ # line at index 0 in the context array is (line.line_number.right - 1)
66
+ return [line.line_number.right - 1 + i, m.captures[0]]
67
+ end
68
+ end
69
+ nil
70
+ end
71
+
72
+ def max(a, b)
73
+ return a if b.nil?
74
+ return b if a.nil?
75
+ a > b ? a : b
76
+ end
77
+ end
78
+ end