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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rspec +3 -0
- data/.rubocop.yml +186 -0
- data/.ruby-version +1 -0
- data/.spellr.yml +23 -0
- data/.spellr_wordlists/dictionary.txt +120 -0
- data/.spellr_wordlists/english.txt +3 -0
- data/.spellr_wordlists/lorem.txt +4 -0
- data/.spellr_wordlists/ruby.txt +2 -0
- data/.travis.yml +7 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +64 -0
- data/Rakefile +8 -0
- data/bin/console +8 -0
- data/bin/fetch_wordlist/english +65 -0
- data/bin/fetch_wordlist/ruby +150 -0
- data/bin/setup +3 -0
- data/exe/spellr +5 -0
- data/lib/.spellr.yml +93 -0
- data/lib/spellr.rb +26 -0
- data/lib/spellr/check.rb +56 -0
- data/lib/spellr/cli.rb +205 -0
- data/lib/spellr/column_location.rb +49 -0
- data/lib/spellr/config.rb +105 -0
- data/lib/spellr/file.rb +27 -0
- data/lib/spellr/file_list.rb +45 -0
- data/lib/spellr/interactive.rb +191 -0
- data/lib/spellr/language.rb +104 -0
- data/lib/spellr/line_location.rb +29 -0
- data/lib/spellr/line_tokenizer.rb +181 -0
- data/lib/spellr/reporter.rb +27 -0
- data/lib/spellr/string_format.rb +43 -0
- data/lib/spellr/token.rb +83 -0
- data/lib/spellr/tokenizer.rb +72 -0
- data/lib/spellr/version.rb +5 -0
- data/lib/spellr/wordlist.rb +100 -0
- data/lib/spellr/wordlist_reporter.rb +21 -0
- data/spellr.gemspec +35 -0
- data/wordlist +2 -0
- data/wordlists/dockerfile.txt +21 -0
- data/wordlists/html.txt +340 -0
- data/wordlists/javascript.txt +64 -0
- data/wordlists/ruby.txt +2344 -0
- data/wordlists/shell.txt +2 -0
- metadata +217 -0
data/lib/spellr/check.rb
ADDED
@@ -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
|
data/lib/spellr/cli.rb
ADDED
@@ -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
|
data/lib/spellr/file.rb
ADDED
@@ -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
|