spellr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +186 -0
  5. data/.ruby-version +1 -0
  6. data/.spellr.yml +23 -0
  7. data/.spellr_wordlists/dictionary.txt +120 -0
  8. data/.spellr_wordlists/english.txt +3 -0
  9. data/.spellr_wordlists/lorem.txt +4 -0
  10. data/.spellr_wordlists/ruby.txt +2 -0
  11. data/.travis.yml +7 -0
  12. data/Gemfile +8 -0
  13. data/Gemfile.lock +67 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +64 -0
  16. data/Rakefile +8 -0
  17. data/bin/console +8 -0
  18. data/bin/fetch_wordlist/english +65 -0
  19. data/bin/fetch_wordlist/ruby +150 -0
  20. data/bin/setup +3 -0
  21. data/exe/spellr +5 -0
  22. data/lib/.spellr.yml +93 -0
  23. data/lib/spellr.rb +26 -0
  24. data/lib/spellr/check.rb +56 -0
  25. data/lib/spellr/cli.rb +205 -0
  26. data/lib/spellr/column_location.rb +49 -0
  27. data/lib/spellr/config.rb +105 -0
  28. data/lib/spellr/file.rb +27 -0
  29. data/lib/spellr/file_list.rb +45 -0
  30. data/lib/spellr/interactive.rb +191 -0
  31. data/lib/spellr/language.rb +104 -0
  32. data/lib/spellr/line_location.rb +29 -0
  33. data/lib/spellr/line_tokenizer.rb +181 -0
  34. data/lib/spellr/reporter.rb +27 -0
  35. data/lib/spellr/string_format.rb +43 -0
  36. data/lib/spellr/token.rb +83 -0
  37. data/lib/spellr/tokenizer.rb +72 -0
  38. data/lib/spellr/version.rb +5 -0
  39. data/lib/spellr/wordlist.rb +100 -0
  40. data/lib/spellr/wordlist_reporter.rb +21 -0
  41. data/spellr.gemspec +35 -0
  42. data/wordlist +2 -0
  43. data/wordlists/dockerfile.txt +21 -0
  44. data/wordlists/html.txt +340 -0
  45. data/wordlists/javascript.txt +64 -0
  46. data/wordlists/ruby.txt +2344 -0
  47. data/wordlists/shell.txt +2 -0
  48. metadata +217 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spellr'
4
+ require_relative 'tokenizer'
5
+ require_relative 'token'
6
+ require_relative 'column_location'
7
+ require_relative 'line_location'
8
+
9
+ module Spellr
10
+ class InvalidByteSequence
11
+ def self.===(error)
12
+ error.is_a?(ArgumentError) &&
13
+ /invalid byte sequence/.match?(error.message)
14
+ end
15
+ end
16
+
17
+ class Check
18
+ attr_reader :exit_code
19
+ attr_reader :files, :reporter
20
+
21
+ def initialize(files: [], reporter: Spellr.config.reporter)
22
+ @files = files
23
+ @reporter = reporter
24
+ @exit_code = 0
25
+ end
26
+
27
+ def check
28
+ checked = 0
29
+ files.each do |file|
30
+ check_file(file)
31
+ checked += 1
32
+ rescue InvalidByteSequence
33
+ # sometimes files are binary
34
+ puts "Skipped unreadable file: #{file}" unless Spellr.config.quiet?
35
+ end
36
+
37
+ reporter.finish(checked) if reporter.respond_to?(:finish)
38
+ end
39
+
40
+ private
41
+
42
+ def check_file(file, start_at: nil, wordlists: Spellr.config.wordlists_for(file))
43
+ Spellr::Tokenizer.new(file, start_at: start_at).each_token do |token|
44
+ next if wordlists.any? { |d| d.include?(token) }
45
+
46
+ start_at = token.location
47
+ reporter.call(token)
48
+ @exit_code = 1
49
+ end
50
+ rescue Spellr::DidReplacement => e # Yeah this is exceptions for control flow, but it makes sense to me
51
+ check_file(file, start_at: e.token.location, wordlists: wordlists)
52
+ rescue Spellr::DidAdd => e
53
+ check_file(file, start_at: e.token.location) # don't cache the wordlists
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'pathname'
5
+ require 'open3'
6
+
7
+ require_relative '../spellr'
8
+
9
+ module Spellr
10
+ class CLI # rubocop:disable Metrics/ClassLength
11
+ attr_writer :fetch_output_dir
12
+ attr_reader :argv
13
+
14
+ def initialize(argv)
15
+ @argv = argv
16
+
17
+ parse_command
18
+ end
19
+
20
+ def check
21
+ require_relative 'check'
22
+ checker = Spellr::Check.new(files: files)
23
+ checker.check
24
+
25
+ exit checker.exit_code
26
+ end
27
+
28
+ def files
29
+ require_relative 'file_list'
30
+ Spellr::FileList.new(*argv)
31
+ end
32
+
33
+ def wordlist_option(_)
34
+ require_relative 'wordlist_reporter'
35
+ Spellr.config.reporter = Spellr::WordlistReporter.new
36
+ end
37
+
38
+ def quiet_option(_)
39
+ Spellr.config.quiet = true
40
+ Spellr.config.reporter = ->(_) {}
41
+ end
42
+
43
+ def interactive_option(_)
44
+ require_relative 'interactive'
45
+ Spellr.config.reporter = Spellr::Interactive.new
46
+ end
47
+
48
+ def config_option(file)
49
+ Spellr.config.config_file = Pathname.pwd.join(file).expand_path
50
+ end
51
+
52
+ def dry_run_option(_)
53
+ files.each { |f| puts f.relative_path_from(Pathname.pwd) }
54
+
55
+ exit
56
+ end
57
+
58
+ def version_option(_)
59
+ require_relative 'version'
60
+ puts(Spellr::VERSION)
61
+
62
+ exit
63
+ end
64
+
65
+ def get_wordlist_option(command)
66
+ get_wordlist_dir.join(command)
67
+ end
68
+
69
+ def fetch_output_dir
70
+ @fetch_output_dir ||= Pathname.pwd.join('.spellr_wordlists/generated').expand_path
71
+ end
72
+
73
+ def fetch_words_for_wordlist(wordlist)
74
+ wordlist_command(wordlist, *argv)
75
+ end
76
+
77
+ def wordlist_command(wordlist, *args)
78
+ require 'shellwords'
79
+ command = fetch_wordlist_dir.join(wordlist).to_s
80
+ fetch_output_dir.mkpath
81
+
82
+ command_with_args = args.unshift(command).shelljoin
83
+
84
+ out, err, status = Open3.capture3(command_with_args)
85
+ puts err unless err.empty?
86
+ return out if status.exitstatus == 0
87
+
88
+ exit
89
+ end
90
+
91
+ def replace_wordlist(words, wordlist)
92
+ require_relative '../../lib/spellr/wordlist'
93
+
94
+ Spellr::Wordlist.new(fetch_output_dir.join("#{wordlist}.txt")).clean(StringIO.new(words))
95
+ end
96
+
97
+ def extract_and_write_license(words, wordlist)
98
+ words, license = words.split('---', 2).reverse
99
+
100
+ fetch_output_dir.join("#{wordlist}.LICENSE.txt").write(license) if license
101
+
102
+ words
103
+ end
104
+
105
+ def fetch
106
+ wordlist = argv.shift
107
+ puts "Fetching #{wordlist} wordlist"
108
+ words = fetch_words_for_wordlist(wordlist)
109
+ puts "Preparing #{wordlist} wordlist"
110
+ words = extract_and_write_license(words, wordlist)
111
+ puts "cleaning #{wordlist} wordlist"
112
+ replace_wordlist(words, wordlist)
113
+ end
114
+
115
+ def output_option(dir)
116
+ self.fetch_output_dir = Pathname.pwd.join(dir).expand_path
117
+ end
118
+
119
+ def wordlists
120
+ fetch_wordlist_dir.children.map { |p| p.basename.to_s }
121
+ end
122
+
123
+ def fetch_wordlist_dir
124
+ @fetch_wordlist_dir ||= Pathname.new(__dir__).parent.parent.join('bin', 'fetch_wordlist').expand_path
125
+ end
126
+
127
+ def parse_command
128
+ case argv.first
129
+ when 'fetch'
130
+ parse_fetch_options
131
+ fetch
132
+ else
133
+ parse_options
134
+ check
135
+ end
136
+ end
137
+
138
+ def fetch_options
139
+ @fetch_options ||= begin
140
+ opts = OptionParser.new
141
+ opts.banner = "Usage: spellr fetch [options] WORDLIST [wordlist options]\nAvailable wordlists: #{wordlists}"
142
+
143
+ opts.separator('')
144
+ opts.on('-o', '--output=OUTPUT', 'Outputs the fetched wordlist to OUTPUT/WORDLIST.txt', &method(:output_option))
145
+ opts.on('-h', '--help', 'Shows help for fetch', &method(:fetch_options_help))
146
+
147
+ opts
148
+ end
149
+ end
150
+
151
+ def fetch_options_help(*_)
152
+ puts fetch_options.help
153
+
154
+ wordlist = argv.first
155
+ if wordlist
156
+ puts
157
+ wordlist_command('english', '--help')
158
+ end
159
+
160
+ exit
161
+ end
162
+
163
+ def options_help(_)
164
+ puts options.help
165
+ puts
166
+ puts fetch_options.help
167
+
168
+ exit
169
+ end
170
+
171
+ def parse_options
172
+ options.parse!(argv)
173
+ end
174
+
175
+ def parse_fetch_options
176
+ argv.shift
177
+ fetch_options.order!(argv) do |non_arg|
178
+ argv.unshift(non_arg)
179
+ break
180
+ end
181
+ end
182
+
183
+ def options # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
184
+ @options ||= begin
185
+ opts = OptionParser.new
186
+
187
+ opts.banner = 'Usage: spellr [options] [files]'
188
+ opts.separator('')
189
+ opts.on('-w', '--wordlist', 'Outputs errors in wordlist format', &method(:wordlist_option))
190
+ opts.on('-q', '--quiet', 'Silences output', &method(:quiet_option))
191
+ opts.on('-i', '--interactive', 'Runs the spell check interactively', &method(:interactive_option))
192
+ opts.separator('')
193
+ opts.on('-d', '--dry-run', 'List files to be checked', &method(:dry_run_option))
194
+ opts.separator('')
195
+ opts.on('-c', '--config FILENAME', String, <<~HELP, &method(:config_option))
196
+ Path to the config file (default ./.spellr.yml)
197
+ HELP
198
+ opts.on('-v', '--version', 'Returns the current version', &method(:version_option))
199
+ opts.on('-h', '--help', 'Shows this message', &method(:options_help))
200
+
201
+ opts
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'line_location'
4
+
5
+ module Spellr
6
+ class ColumnLocation
7
+ attr_reader :line_location
8
+ attr_reader :char_offset
9
+ attr_reader :byte_offset
10
+
11
+ def initialize(char_offset: 0, byte_offset: 0, line_location: LineLocation.new)
12
+ @line_location = line_location
13
+ @char_offset = char_offset
14
+ @byte_offset = byte_offset
15
+ end
16
+
17
+ def absolute_char_offset
18
+ char_offset + line_location.char_offset
19
+ end
20
+
21
+ def absolute_byte_offset
22
+ byte_offset + line_location.byte_offset
23
+ end
24
+
25
+ def line_number
26
+ line_location.line_number
27
+ end
28
+
29
+ def file
30
+ line_location.file
31
+ end
32
+
33
+ def file_name
34
+ line_location.file_name
35
+ end
36
+
37
+ def to_s
38
+ "#{line_location}:#{char_offset}"
39
+ end
40
+
41
+ def inspect
42
+ "#<#{self.class.name} #{self}>"
43
+ end
44
+
45
+ def coordinates
46
+ [line_number, char_offset]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spellr'
4
+
5
+ module Spellr
6
+ class Config
7
+ attr_writer :reporter
8
+ attr_reader :config_file
9
+ attr_accessor :quiet
10
+ alias_method :quiet?, :quiet
11
+
12
+ def initialize
13
+ @config_file = ::File.join(Dir.pwd, '.spellr.yml')
14
+ load_config
15
+ end
16
+
17
+ def word_minimum_length
18
+ @config[:word_minimum_length]
19
+ end
20
+
21
+ def only
22
+ @config[:only] || []
23
+ end
24
+
25
+ def ignored
26
+ @config[:ignore]
27
+ end
28
+
29
+ def color
30
+ @config[:color]
31
+ end
32
+
33
+ def clear_cache
34
+ remove_instance_variable(:@wordlists) if defined?(@wordlists)
35
+ remove_instance_variable(:@languages) if defined?(@languages)
36
+ end
37
+
38
+ def languages
39
+ require_relative 'language'
40
+
41
+ @languages ||= @config[:languages].map do |key, args|
42
+ [key, Spellr::Language.new(key, args)]
43
+ end.to_h
44
+ end
45
+
46
+ def languages_for(file)
47
+ languages.values.select { |l| l.matches?(file) }
48
+ end
49
+
50
+ def wordlists
51
+ @wordlists ||= languages.values.flat_map(&:wordlists)
52
+ end
53
+
54
+ def all_wordlist_paths
55
+ languages.values.flat_map(&:all_wordlist_paths)
56
+ end
57
+
58
+ def wordlists_for(file)
59
+ languages_for(file).flat_map(&:wordlists)
60
+ end
61
+
62
+ def config_file=(value)
63
+ ::File.read(value) # raise Errno::ENOENT if the file doesn't exist
64
+ @config_file = value
65
+ load_config
66
+ end
67
+
68
+ def reporter
69
+ @reporter ||= default_reporter
70
+ end
71
+
72
+ private
73
+
74
+ def default_reporter
75
+ require_relative 'reporter'
76
+
77
+ Spellr::Reporter.new
78
+ end
79
+
80
+ def load_config
81
+ default_config = load_yaml(::File.join(__dir__, '..', '.spellr.yml'))
82
+ project_config = load_yaml(config_file)
83
+
84
+ @config = merge_config(default_config, project_config)
85
+ end
86
+
87
+ def load_yaml(path)
88
+ require 'yaml'
89
+
90
+ return {} unless ::File.exist?(path)
91
+
92
+ YAML.safe_load(::File.read(path), symbolize_names: true)
93
+ end
94
+
95
+ def merge_config(default, project)
96
+ if project.is_a?(Array) && default.is_a?(Array)
97
+ default | project
98
+ elsif project.is_a?(Hash) && default.is_a?(Hash)
99
+ default.merge(project) { |_k, d, p| merge_config(d, p) }
100
+ else
101
+ project
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Spellr
6
+ class File < Pathname
7
+ def self.wrap(file)
8
+ file.is_a?(Spellr::File) ? file : Spellr::File.new(file)
9
+ end
10
+
11
+ def hashbang
12
+ return if extname != ''
13
+ return unless first_line&.start_with?('#!')
14
+
15
+ first_line
16
+ end
17
+
18
+ def first_line
19
+ @first_line ||= each_line.first
20
+ end
21
+
22
+ def fnmatch?(pattern)
23
+ relative_path_from(Pathname.pwd).fnmatch?(pattern, ::File::FNM_DOTMATCH) ||
24
+ Pathname.new(basename).fnmatch?(pattern, ::File::FNM_DOTMATCH)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fast_ignore'
4
+ require_relative '../spellr'
5
+ require_relative 'file'
6
+
7
+ module Spellr
8
+ class FileList
9
+ include Enumerable
10
+
11
+ def initialize(*patterns)
12
+ @patterns = patterns
13
+ end
14
+
15
+ def wordlist?(file)
16
+ Spellr.config.all_wordlist_paths.any? { |w| w == file }
17
+ end
18
+
19
+ def config_only?(file)
20
+ Spellr.config.only.empty? || Spellr.config.only.any? { |o| file.fnmatch?(o) }
21
+ end
22
+
23
+ def cli_only?(file)
24
+ @patterns.empty? || @patterns.any? { |p| file.fnmatch?(p) }
25
+ end
26
+
27
+ def each
28
+ # TODO: handle no gitignore
29
+ gitignore = ::File.join(Dir.pwd, '.gitignore')
30
+ gitignore = nil unless ::File.exist?(gitignore)
31
+ FastIgnore.new(rules: Spellr.config.ignored, gitignore: gitignore).each do |file|
32
+ file = Spellr::File.new(file)
33
+ next unless cli_only?(file)
34
+ next if wordlist?(file)
35
+ next unless config_only?(file)
36
+
37
+ yield(file)
38
+ end
39
+ end
40
+
41
+ def to_a
42
+ enum_for(:each).to_a
43
+ end
44
+ end
45
+ end