danger-spelling 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,370 @@
1
+ require 'English'
2
+ require 'uri'
3
+
4
+ module Danger
5
+ # This is a Danger plugin that wraps the python library pyspelling and some of its usage.
6
+ # The pyspelling results are posted to the pull request as a comment with the spelling mistake, file path &
7
+ # line number where the spelling mistake was found.
8
+ #
9
+ # It has some dependencies that should be installed prior to running.
10
+ #
11
+ # * [pyspelling](https://facelessuser.github.io/pyspelling/)
12
+ # * [aspell](http://aspell.net)
13
+ # * **OR**
14
+ # * [hunspell](http://hunspell.github.io)
15
+ #
16
+ # Your repository will also require a .pyspelling.yml file to be present. This .pyspelling.yml can be basic,
17
+ # but it will require a name and source property. Its advisable to include `expect_match: false` in your test
18
+ # matrix. This will stop pyspelling from generating an error at runtime.
19
+ #
20
+ # There are several ways to use this danger plugin
21
+ #
22
+ # @example execute pyspelling matrix with the name 'test_matrix' on all files modified or
23
+ # added in the given pull request.
24
+ # spelling.name = "test_matrix"
25
+ # spelling.check_spelling
26
+ #
27
+ # @see HammyAssassin/danger-spelling
28
+ # @tags spelling, danger, pyspelling, hunspell, aspell
29
+ #
30
+ #
31
+ # @example execute pyspelling matrix with the name 'test_matrix' on all files modified or added in the given pull
32
+ # request, excluding some specific file names
33
+ # spelling.ignored_files = ["Gemfile"]
34
+ # spelling.name = "test_matrix"
35
+ # spelling.check_spelling
36
+ #
37
+ # @see HammyAssassin/danger-spelling
38
+ # @tags spelling, danger, pyspelling, hunspell, aspell
39
+ #
40
+ #
41
+ # @example execute pyspelling matrix with the name 'test_matrix' on all files modified or added in the given pull
42
+ # request, excluding some specific file names and excluding some words
43
+ # spelling.ignored_words = ["HammyAssassin"]
44
+ # spelling.ignored_files = ["Gemfile"]
45
+ # spelling.name = "test_matrix"
46
+ # spelling.check_spelling
47
+ #
48
+ # @see HammyAssassin/danger-spelling
49
+ # @tags spelling, danger, pyspelling, hunspell, aspell
50
+ #
51
+ class DangerSpelling < Plugin
52
+ # Allows you to ignore certain words that might otherwise be detected as a spelling error.
53
+ # default value is [] when its nil
54
+ #
55
+ # @return [Array<String>]
56
+ attr_accessor :ignored_words
57
+
58
+ # Allows you to ignore certain files that might otherwise be scanned by pyspelling.
59
+ # The default value is [] for when its nil
60
+ #
61
+ # @return [Array<String>]
62
+ attr_accessor :ignored_files
63
+
64
+ # **required** The name of the test matrix in your .pyspelling.yml
65
+ # An exception will be raised if this is not specified in your Danger file.
66
+ #
67
+ # @return [<String>]
68
+ attr_accessor :name
69
+
70
+ # Checks the spelling of all files added or modified in a given pull request. This will fail if
71
+ # pyspelling cannot be installed if not installed already. It will fail if `aspell` or `hunspell`
72
+ # are not detected.
73
+ #
74
+ # It will also fail if the required parameter `name` hasn't been specificed in the Danger file.
75
+ #
76
+ #
77
+ # @param [<Danger::FileList>] files **Optional** files to be scanned. Default value is nil. If nil, added and
78
+ # modified files will be scanned.
79
+ #
80
+ # @return [void]
81
+ #
82
+ def check_spelling(files = nil)
83
+ raise 'name must be a valid matrix name in your .pyspelling.yml.' if name.nil? || name.empty?
84
+
85
+ check_for_dependancies
86
+
87
+ new_files = get_files files
88
+ results_texts = pyspelling_results(new_files)
89
+
90
+ spell_issues = results_texts.select { |_, output| output.include? 'Spelling check failed' }
91
+
92
+ # Get some metadata about the local setup
93
+ current_slug = env.ci_source.repo_slug
94
+
95
+ update_message_for_issues(spell_issues, current_slug) if spell_issues.count.positive?
96
+ end
97
+
98
+ #
99
+ #
100
+ # **Internal Method**
101
+ #
102
+ # Updates the message that will eventually be posted as a comment a pull request with
103
+ # a new line for each time the spelling error has been detected.
104
+ #
105
+ # @param [<Hash>] spell_issues the Hash containing the file path & the detected mistakes.
106
+ # @param [<String>] current_slug the repo. eg /hamstringassassin/danger-spelling.
107
+ #
108
+ # @return [Array<String>] an array of messages to be displayed in the PR comment.
109
+ #
110
+ def update_message_for_issues(spell_issues, current_slug)
111
+ message = "### Spell Checker found issues\n\n"
112
+
113
+ spell_issues.each do |path, output|
114
+ git_loc = git_check(current_slug, path)
115
+ error_message = ''
116
+ error_message_updated = false
117
+ error_message = message_title(error_message, path, git_loc)
118
+
119
+ output_array = output.split(/\n/)
120
+ output_array = remove_ignored_words(output_array, path)
121
+
122
+ output_array.each do |txt|
123
+ File.open(path, 'r') do |file_handle|
124
+ file_handle.each_line do |path_line|
125
+ if find_word_in_text(path_line, txt)
126
+ error_message << "#{$INPUT_LINE_NUMBER} | #{txt} \n "
127
+ error_message_updated = true
128
+ end
129
+ end
130
+ end
131
+ end
132
+ if error_message_updated
133
+ message << error_message
134
+ error_message = ''
135
+ end
136
+ end
137
+ markdown message
138
+ end
139
+
140
+ #
141
+ #
142
+ # **Internal Method**
143
+ #
144
+ #
145
+ # appends the default message when a spelling error is found.
146
+ #
147
+ # @param [<String>] message the message to append
148
+ # @param [<String>] path the path of the file
149
+ # @param [<String>] git_loc the git location of the file
150
+ #
151
+ # @return [<String>] formatted message
152
+ #
153
+ def message_title(message, path, git_loc)
154
+ message << "#### [#{path}](#{git_loc})\n\n"
155
+ message << "Line | Typo |\n "
156
+ message << "| --- | ------ |\n "
157
+ message
158
+ end
159
+
160
+ #
161
+ #
162
+ # **Internal Method**
163
+ #
164
+ # splits a line of text up and checks if the spelling error is a match.
165
+ #
166
+ # @param [<String>] text the string to be split and checked.
167
+ # @param [<String>] word the word to find in text.
168
+ #
169
+ # @return [<Bool>] if the word is found.
170
+ #
171
+ def find_word_in_text(text, word)
172
+ val = false
173
+ line_array = text.split
174
+ line_array.each do |array_item|
175
+ array_item = array_item[0...array_item.size - 1] if array_item[-1] == '.'
176
+ # puts "array_item #{array_item}"
177
+ # puts "is url #{is_url(array_item.strip)}"
178
+ if array_item.strip == word.strip && !url?(array_item.strip)
179
+ val = true
180
+ val
181
+ end
182
+ end
183
+ val
184
+ end
185
+
186
+ #
187
+ #
188
+ # **Internal Method**
189
+ #
190
+ # checks if a given String is a URL
191
+ #
192
+ # @param [<String>] txt String to check
193
+ #
194
+ # @return [<Bool>]
195
+ #
196
+ def url?(txt)
197
+ txt =~ /\A#{URI::DEFAULT_PARSER.make_regexp}\z/
198
+ end
199
+
200
+ #
201
+ #
202
+ # **Internal Method**
203
+ #
204
+ # Runs pyspelling on the test matrix name provided with any files given.
205
+ #
206
+ # @param [<Danger::FileList>] new_files a list of files provided to scan with pyspelling.
207
+ #
208
+ # @return [Hash] returns a hash of the file scanned and any spelling errors found.
209
+ #
210
+ def pyspelling_results(new_files)
211
+ results_texts = {}
212
+ new_files.each do |file|
213
+ file_result = `pyspelling --name '#{name}' --source '#{file}'`
214
+ results_texts[file] = file_result
215
+ end
216
+ results_texts
217
+ end
218
+
219
+ #
220
+ #
221
+ # **Internal Method**
222
+ #
223
+ # Check on the git service used. Will raise an error if using bitbucket as it currently doesnt support that.
224
+ #
225
+ # @param [<String>] current_slug the current repo slug. eg. hamstringassassin/danger-spelling.
226
+ # @param [<String>] path path to file.
227
+ #
228
+ # @return [<String>] full path to file including branch.
229
+ #
230
+ def git_check(current_slug, path)
231
+ if defined? @dangerfile.github
232
+ "/#{current_slug}/tree/#{github.branch_for_head}/#{path}"
233
+ elsif defined? @dangerfile.gitlab
234
+ "/#{current_slug}/tree/#{gitlab.branch_for_head}/#{path}"
235
+ else
236
+ raise 'This plugin does not yet support bitbucket'
237
+ end
238
+ end
239
+
240
+ #
241
+ #
242
+ # **Internal Method**
243
+ #
244
+ # Check for dependencies. Raises exception if pyspelling, hunspell or aspell are not installed.
245
+ #
246
+ # @return [Void]
247
+ #
248
+ def check_for_dependancies
249
+ raise 'pyspelling is not in the users PATH, or it failed to install.' unless pyspelling_installed?
250
+
251
+ raise 'aspell or hunspell must be installed in order for pyspelling to work.' unless aspell_hunspell_installed?
252
+ end
253
+
254
+ #
255
+ #
256
+ # **Internal Method**
257
+ #
258
+ # Checks if a given line can be ignored if it contains expected pyspelling output.
259
+ #
260
+ # @param [<String>] text the text to check.
261
+ # @param [<String>] file_path the file path to check.
262
+ #
263
+ # @return [<Bool>] if the line can be ignored.
264
+ #
265
+ def ignore_line(text, file_path)
266
+ text.strip == 'Misspelled words:' ||
267
+ text.strip == "<text> #{file_path}" ||
268
+ text.strip == '!!!Spelling check failed!!!' ||
269
+ text.strip == '--------------------------------------------------------------------------------' ||
270
+ text.strip == ''
271
+ end
272
+
273
+ #
274
+ #
275
+ # **Internal Method**
276
+ #
277
+ # Removes some standard words in the pyspelling results.
278
+ # Words provided in `ignored_words` will also be removed from the results array.
279
+ #
280
+ # @param [<Array>] spelling_errors Complete list of spelling errors.
281
+ # @param [<String>] file_path file path.
282
+ #
283
+ # @return [<Array>] curated list of spelling errors, excluding standard and user defined words.
284
+ #
285
+ def remove_ignored_words(spelling_errors, file_path)
286
+ spelling_errors.delete('Misspelled words:')
287
+ spelling_errors.delete("<text> #{file_path}".strip)
288
+ spelling_errors.delete('!!!Spelling check failed!!!')
289
+ spelling_errors.delete('--------------------------------------------------------------------------------')
290
+ spelling_errors.delete('')
291
+ ignored_words.each do |word|
292
+ spelling_errors.delete(word)
293
+ end
294
+ spelling_errors
295
+ end
296
+
297
+ #
298
+ #
299
+ # **Internal Method**
300
+ #
301
+ # Checks of pyspelling is installed.
302
+ #
303
+ # @return [<Bool>]
304
+ #
305
+ def pyspelling_installed?
306
+ 'which pyspelling'.strip.empty? == false
307
+ end
308
+
309
+ #
310
+ #
311
+ # **Internal Method**
312
+ #
313
+ # Checks if aspell is installed.
314
+ #
315
+ # @return [<Bool>]
316
+ #
317
+ def aspell_installed?
318
+ 'which aspell'.strip.empty? == false
319
+ end
320
+
321
+ #
322
+ #
323
+ # **Internal Method**
324
+ #
325
+ # Checks if Hunspell is installed.
326
+ #
327
+ # @return [<Bool>]
328
+ #
329
+ def hunspell_installed?
330
+ 'which hunspell'.strip.empty? == false
331
+ end
332
+
333
+ #
334
+ #
335
+ # **Internal Method**
336
+ #
337
+ # checks if aspell and hunspell are installed.
338
+ #
339
+ # @return [<Bool>]
340
+ #
341
+ def aspell_hunspell_installed?
342
+ aspell_installed? && hunspell_installed?
343
+ end
344
+
345
+ #
346
+ #
347
+ # **Internal Method**
348
+ #
349
+ # Gets a file list of the files provided or finds modified and added files to scan.
350
+ # If files are provided via `ignored_files` they will be removed from the final returned
351
+ # list.
352
+ #
353
+ # Will raise an exception if no files are found.
354
+ #
355
+ # @param [<Danger::FileList>] files FileList to scan. Can be nil.
356
+ #
357
+ # @return [<Danger::FileList>] a FileList of files found.
358
+ #
359
+ def get_files(files)
360
+ # Use either the files provided, or the modified & added files.
361
+ found_files = files ? Dir.glob(files) : (git.modified_files + git.added_files)
362
+ raise 'No files found to check' if found_files.nil?
363
+
364
+ ignored_files.each do |file|
365
+ found_files.delete(file)
366
+ end
367
+ found_files
368
+ end
369
+ end
370
+ end