spellr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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