-
Notifications
You must be signed in to change notification settings - Fork 11
/
danger_plugin.rb
201 lines (169 loc) · 7.17 KB
/
danger_plugin.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
require 'json'
module Danger
# Lint markdown files inside your projects.
# This is done using the [proselint](http://proselint.com) python egg.
# Results are passed out as a table in markdown.
#
# @example Running linter with custom disabled linters
#
# # Runs a linter with comma style and tense present disabled
# prose.disable_linters = ["misc.scare_quotes", "misc.tense_present"]
# prose.lint_files "_posts/*.md"
#
# @example Running linter with default linters
#
# # Runs a linter with all styles, on modified and added markdown files in this PR
# prose.lint_files
#
# @example Running the spell checker
#
# # Runs a spell checker on all files in `_post`
# prose.check_spelling "_posts/*.md"
#
# @example Running the spell checker, with some words whitelisted
#
# prose.ignored_words = ["orta", "artsy"]
# prose.lint_files
#
# @see artsy/artsy.github.io
# @tags blogging, blog, writing, jekyll, middleman, hugo, metalsmith, gatsby, express
#
class DangerProse < Plugin
# Allows you to disable a collection of linters from running. Doesn't work yet.
# You can get a list of [them here](https://github.com/amperser/proselint#checks)
# defaults to `["misc.scare_quotes", "typography.symbols"]` when it's nil.
#
# @return [Array<String>]
attr_accessor :disable_linters
# Lints the globbed markdown files. Will fail if `proselint` cannot be installed correctly.
# Generates a `markdown` list of warnings for the prose in a corpus of .markdown and .md files.
#
# @param [String] files
# A globbed string which should return the files that you want to lint, defaults to nil.
# if nil, modified and added files from the diff will be used.
# @return [void]
#
def lint_files(files = nil)
# Installs a prose checker if needed
system 'pip install --user proselint' unless proselint_installed?
# Check that this is in the user's PATH after installing
raise "proselint is not in the user's PATH, or it failed to install" unless proselint_installed?
# Either use files provided, or use the modified + added
markdown_files = get_files files
proses = {}
to_disable = disable_linters || ["misc.scare_quotes", "typography.symbols"]
with_proselint_disabled(to_disable) do
# Convert paths to proselint results
result_jsons = Hash[markdown_files.uniq.collect { |v| [v, get_proselint_json(v)] }]
proses = result_jsons.select { |_, prose| prose['data']['errors'].count }
end
# Get some metadata about the local setup
current_slug = env.ci_source.repo_slug
# We got some error reports back from proselint
if proses.count > 0
message = "### Proselint found issues\n\n"
proses.each do |path, prose|
github_loc = "/#{current_slug}/tree/#{github.branch_for_head}/#{path}"
message << "#### [#{path}](#{github_loc})\n\n"
message << "Line | Message | Severity |\n"
message << "| --- | ----- | ----- |\n"
prose['data']['errors'].each do |error|
message << "#{error['line']} | #{error['message']} | #{error['severity']}\n"
end
end
markdown message
end
end
# Determine if proselint is currently installed in the system paths.
# @return [Bool]
#
def proselint_installed?
`which proselint`.strip.empty? == false
end
# Determine if mdspell is currently installed in the system paths.
# @return [Bool]
#
def mdspell_installed?
`which mdspell`.strip.empty? == false
end
# Allows you to add a collection of words to skip in spellchecking.
# defaults to `[""]` when it's nil.
# @return [Array<String>]
attr_accessor :ignored_words
# Runs a markdown-specific spell checker, against a corpus of `.markdown` and `.md` files.
#
# @param [String] files
# A globbed string which should return the files that you want to spell check, defaults to nil.
# if nil, modified and added files from the diff will be used.
# @return [void]
#
def check_spelling(files = nil)
# Installs my fork of the spell checker if needed
# my fork has line numbers + indexes
system "npm install -g orta/node-markdown-spellcheck" unless mdspell_installed?
# Check that this is in the user's PATH after installing
raise "mdspell is not in the user's PATH, or it failed to install" unless mdspell_installed?
markdown_files = get_files files
skip_words = ignored_words || []
File.write(".spelling", skip_words.join("\n"))
result_texts = Hash[markdown_files.uniq.collect { |md| [md, `mdspell #{md} -r`.strip] }]
spell_issues = result_texts.select { |path, output| output.include? "spelling errors found" }
File.unlink(".spelling")
# Get some metadata about the local setup
current_slug = env.ci_source.repo_slug
if spell_issues.count > 0
message = "### Spell Checker found issues\n\n"
spell_issues.each do |path, output|
github_loc = "/#{current_slug}/tree/#{github.branch_for_head}/#{path}"
message << "#### [#{path}](#{github_loc})\n\n"
message << "Line | Typo |\n"
message << "| --- | ------ |\n"
output.lines[1..-3].each do |line|
index_info = line.strip.split("|").first
index_line, index = index_info.split(":").map { |n| n.to_i }
file = File.read(path)
unknown_word = file[index..-1].split(" ").first
error_text = line.strip.split("|")[1..-1].join("|").strip
error = error_text.gsub(unknown_word, "**" + unknown_word + "**")
message << "#{index_line} | #{error} | \n"
end
markdown message
end
end
end
private
# Creates a temporary proselint settings file
# @return void
#
def with_proselint_disabled(disable_linters)
# Create the disabled linters JSON in ~/.proselintrc
proselint_template = File.join(File.dirname(__FILE__), 'proselintrc')
proselintJSON = JSON.parse(File.read(proselint_template))
# Disable individual linters
disable_linters.each do |linter|
proselintJSON['checks'][linter] = false
end
# Re-save the new JSON into the home dir
temp_proselint_rc_path = File.join(Dir.home, '.proselintrc')
File.write(temp_proselint_rc_path, JSON.pretty_generate(proselintJSON))
# Run the closure
yield
# Delete .proselintrc
File.unlink temp_proselint_rc_path
end
def get_files files
# Either use files provided, or use the modified + added
markdown_files = files ? Dir.glob(files) : (git.modified_files + git.added_files)
markdown_files.select { |line| line.end_with? '.markdown', '.md' }
end
# Always returns a hash, regardless of whether the command gives JSON, weird data, or no response
def get_proselint_json path
json = `proselint "#{path}" --json`.strip
if json[0] == "{" and json[-1] == "}"
JSON.parse json
else
{}
end
end
end
end