spellr 0.5.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/Gemfile.lock +14 -14
- data/lib/.spellr.yml +2 -0
- data/lib/spellr/backports.rb +16 -6
- data/lib/spellr/base_reporter.rb +54 -0
- data/lib/spellr/check.rb +54 -20
- data/lib/spellr/cli.rb +13 -6
- data/lib/spellr/column_location.rb +1 -1
- data/lib/spellr/config.rb +6 -45
- data/lib/spellr/config_loader.rb +10 -6
- data/lib/spellr/file.rb +15 -2
- data/lib/spellr/file_list.rb +21 -17
- data/lib/spellr/interactive.rb +51 -116
- data/lib/spellr/interactive_add.rb +64 -0
- data/lib/spellr/interactive_replacement.rb +69 -0
- data/lib/spellr/key_tuner/naive_bayes.rb +49 -91
- data/lib/spellr/key_tuner/possible_key.rb +36 -32
- data/lib/spellr/key_tuner/stats.rb +26 -7
- data/lib/spellr/language.rb +28 -44
- data/lib/spellr/line_location.rb +13 -7
- data/lib/spellr/line_tokenizer.rb +35 -134
- data/lib/spellr/output.rb +62 -0
- data/lib/spellr/output_stubbed.rb +58 -0
- data/lib/spellr/quiet_reporter.rb +13 -0
- data/lib/spellr/reporter.rb +9 -13
- data/lib/spellr/token.rb +14 -16
- data/lib/spellr/token_regexps.rb +103 -0
- data/lib/spellr/tokenizer.rb +35 -14
- data/lib/spellr/version.rb +1 -1
- data/lib/spellr/wordlist.rb +29 -25
- data/lib/spellr/wordlist_reporter.rb +16 -8
- data/lib/spellr.rb +1 -0
- data/wordlists/ruby.txt +1046 -13
- metadata +9 -2
@@ -1,6 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Stats
|
4
|
+
module_function
|
5
|
+
|
6
|
+
extend Math
|
7
|
+
|
4
8
|
def mean(values, &block)
|
5
9
|
return 0 if values.empty?
|
6
10
|
|
@@ -9,25 +13,40 @@ module Stats
|
|
9
13
|
|
10
14
|
def min(values, &block)
|
11
15
|
return 0 if values.empty?
|
16
|
+
return values.min unless block_given?
|
12
17
|
|
13
|
-
|
14
|
-
block.call(values.min_by(&block))
|
18
|
+
yield values.min_by(&block)
|
15
19
|
end
|
16
20
|
|
17
21
|
def max(values, &block)
|
18
22
|
return 0 if values.empty?
|
23
|
+
return values.max unless block_given?
|
19
24
|
|
20
|
-
|
21
|
-
block.call(values.max_by(&block))
|
25
|
+
yield values.max_by(&block)
|
22
26
|
end
|
23
27
|
|
24
|
-
def variance(values, &block)
|
28
|
+
def variance(values, &block) # rubocop:disable Metrics/MethodLength
|
25
29
|
return 0 if values.empty?
|
26
30
|
|
27
|
-
|
31
|
+
mean = mean(values, &block)
|
32
|
+
values.sum do |value|
|
33
|
+
value = yield value if block_given?
|
34
|
+
(mean - value)**2
|
35
|
+
end.to_f / values.length
|
28
36
|
end
|
29
37
|
|
30
38
|
def standard_deviation(values, &block)
|
31
|
-
|
39
|
+
sqrt(variance(values, &block))
|
40
|
+
end
|
41
|
+
|
42
|
+
def gaussian_probability(value, standard_deviation:, mean:, variance:)
|
43
|
+
# deal with the edge case of a 0 standard deviation
|
44
|
+
if standard_deviation == 0
|
45
|
+
return mean == value ? 1.0 : 0.0
|
46
|
+
end
|
47
|
+
|
48
|
+
# calculate the gaussian probability
|
49
|
+
exp = -((value - mean)**2) / (2 * variance)
|
50
|
+
(1.0 / sqrt(2 * Math::PI * variance)) * (Math::E**exp)
|
32
51
|
end
|
33
52
|
end
|
data/lib/spellr/language.rb
CHANGED
@@ -7,52 +7,45 @@ module Spellr
|
|
7
7
|
attr_reader :name
|
8
8
|
attr_reader :key
|
9
9
|
|
10
|
-
def initialize(name,
|
11
|
-
key: name[0],
|
12
|
-
generate: nil,
|
13
|
-
only: [],
|
14
|
-
includes: [],
|
15
|
-
description: '',
|
16
|
-
hashbangs: [],
|
17
|
-
locale: [])
|
18
|
-
unless only.empty?
|
19
|
-
warn <<~WARNING
|
20
|
-
\e[33mSpellr: `only:` language yaml key with a list of fnmatch rules is deprecated.
|
21
|
-
Please use `includes:` instead, which uses gitignore-inspired rules.
|
22
|
-
see github.com/robotdana/fast_ignore#using-an-includes-list for details\e[0m
|
23
|
-
WARNING
|
24
|
-
end
|
25
|
-
|
26
|
-
if generate
|
27
|
-
warn <<~WARNING
|
28
|
-
\e[33mSpellr: `generate:` and generation is now deprecated. Choose the language
|
29
|
-
using the key `locale:` (any of US,AU,CA,GB,GBz,GBs as a string or array).\e[0m
|
30
|
-
WARNING
|
31
|
-
end
|
32
|
-
|
10
|
+
def initialize(name, key: name[0], includes: [], hashbangs: [], locale: [])
|
33
11
|
@name = name
|
34
12
|
@key = key
|
35
|
-
@
|
36
|
-
@includes = only + includes
|
13
|
+
@includes = includes
|
37
14
|
@hashbangs = hashbangs
|
38
15
|
@locales = Array(locale)
|
39
16
|
end
|
40
17
|
|
41
18
|
def matches?(file)
|
42
|
-
|
19
|
+
matches_includes?(file) || matches_hashbangs?(file)
|
20
|
+
end
|
43
21
|
|
44
|
-
|
22
|
+
def wordlists
|
23
|
+
default_wordlists.select(&:exist?)
|
24
|
+
end
|
45
25
|
|
46
|
-
|
47
|
-
|
26
|
+
def project_wordlist
|
27
|
+
@project_wordlist ||= Spellr::Wordlist.new(
|
28
|
+
Pathname.pwd.join('.spellr_wordlists', "#{name}.txt"),
|
29
|
+
name: name
|
30
|
+
)
|
48
31
|
end
|
49
32
|
|
50
|
-
|
51
|
-
|
33
|
+
private
|
34
|
+
|
35
|
+
def matches_hashbangs?(file)
|
36
|
+
return @includes.empty? if @hashbangs.empty?
|
37
|
+
|
38
|
+
file = Spellr::File.wrap(file)
|
39
|
+
return unless file.hashbang
|
40
|
+
|
41
|
+
@hashbangs.any? { |h| file.hashbang.include?(h) }
|
52
42
|
end
|
53
43
|
|
54
|
-
def
|
55
|
-
|
44
|
+
def matches_includes?(file)
|
45
|
+
return @hashbangs.empty? if @includes.empty?
|
46
|
+
|
47
|
+
@fast_ignore ||= FastIgnore.new(include_rules: @includes, gitignore: false)
|
48
|
+
@fast_ignore.allowed?(file.to_s)
|
56
49
|
end
|
57
50
|
|
58
51
|
def gem_wordlist
|
@@ -61,13 +54,6 @@ module Spellr
|
|
61
54
|
)
|
62
55
|
end
|
63
56
|
|
64
|
-
def project_wordlist
|
65
|
-
@project_wordlist ||= Spellr::Wordlist.new(
|
66
|
-
Pathname.pwd.join('.spellr_wordlists', "#{name}.txt"),
|
67
|
-
name: name
|
68
|
-
)
|
69
|
-
end
|
70
|
-
|
71
57
|
def locale_wordlists
|
72
58
|
@locale_wordlists ||= @locales.map do |locale|
|
73
59
|
Spellr::Wordlist.new(
|
@@ -76,8 +62,6 @@ module Spellr
|
|
76
62
|
end
|
77
63
|
end
|
78
64
|
|
79
|
-
private
|
80
|
-
|
81
65
|
def load_wordlists(name, paths)
|
82
66
|
wordlists = paths + default_wordlist_paths(name)
|
83
67
|
|
@@ -87,8 +71,8 @@ module Spellr
|
|
87
71
|
def default_wordlists
|
88
72
|
[
|
89
73
|
gem_wordlist,
|
90
|
-
|
91
|
-
|
74
|
+
*locale_wordlists,
|
75
|
+
project_wordlist
|
92
76
|
]
|
93
77
|
end
|
94
78
|
end
|
data/lib/spellr/line_location.rb
CHANGED
@@ -2,28 +2,34 @@
|
|
2
2
|
|
3
3
|
module Spellr
|
4
4
|
class LineLocation
|
5
|
-
attr_reader :file
|
6
5
|
attr_reader :line_number
|
7
6
|
attr_reader :char_offset
|
8
7
|
attr_reader :byte_offset
|
9
8
|
|
10
9
|
def initialize(file = '[String]', line_number = 1, char_offset: 0, byte_offset: 0)
|
11
|
-
@
|
10
|
+
@filename = file
|
12
11
|
@line_number = line_number
|
13
12
|
@char_offset = char_offset
|
14
13
|
@byte_offset = byte_offset
|
15
14
|
end
|
16
15
|
|
17
16
|
def to_s
|
18
|
-
"#{
|
17
|
+
"#{file_relative_path}:#{line_number}"
|
19
18
|
end
|
20
19
|
|
21
|
-
def
|
22
|
-
file.
|
20
|
+
def file_relative_path
|
21
|
+
file.relative_path
|
23
22
|
end
|
24
23
|
|
25
|
-
def
|
26
|
-
|
24
|
+
def file
|
25
|
+
@file ||= Spellr::File.wrap(@filename)
|
26
|
+
end
|
27
|
+
|
28
|
+
def advance(line)
|
29
|
+
LineLocation.new(@filename,
|
30
|
+
line_number + 1,
|
31
|
+
char_offset: char_offset + line.length,
|
32
|
+
byte_offset: byte_offset + line.bytesize)
|
27
33
|
end
|
28
34
|
end
|
29
35
|
end
|
@@ -5,57 +5,62 @@ require_relative '../spellr'
|
|
5
5
|
require_relative 'column_location'
|
6
6
|
require_relative 'token'
|
7
7
|
require_relative 'key_tuner/naive_bayes'
|
8
|
+
require_relative 'token_regexps'
|
8
9
|
|
9
10
|
module Spellr
|
10
|
-
class LineTokenizer < StringScanner
|
11
|
+
class LineTokenizer < StringScanner
|
11
12
|
attr_reader :line
|
12
13
|
attr_accessor :disabled
|
13
14
|
alias_method :disabled?, :disabled
|
14
|
-
attr_accessor :skip_uri
|
15
|
-
alias_method :skip_uri?, :skip_uri
|
16
15
|
attr_accessor :skip_key
|
17
16
|
alias_method :skip_key?, :skip_key
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
include TokenRegexps
|
19
|
+
|
20
|
+
def initialize(line, skip_key: true)
|
21
|
+
@line = line
|
22
22
|
@skip_key = skip_key
|
23
23
|
|
24
24
|
super(@line.to_s)
|
25
25
|
end
|
26
26
|
|
27
27
|
def string=(line)
|
28
|
-
@line =
|
28
|
+
@line = line
|
29
29
|
super(@line.to_s)
|
30
30
|
end
|
31
31
|
|
32
32
|
def each_term
|
33
33
|
until eos?
|
34
34
|
term = next_term
|
35
|
-
next
|
36
|
-
next if disabled?
|
35
|
+
next if !term || disabled?
|
37
36
|
|
38
37
|
yield term
|
39
38
|
end
|
40
39
|
end
|
41
40
|
|
42
|
-
def each_token
|
41
|
+
def each_token(skip_term_proc: nil) # rubocop:disable Metrics/MethodLength
|
43
42
|
until eos?
|
44
43
|
term = next_term
|
45
44
|
next unless term
|
46
|
-
next if disabled?
|
45
|
+
next if disabled? || skip_term_proc&.call(term)
|
47
46
|
|
48
47
|
yield Token.new(term, line: line, location: column_location(term))
|
49
48
|
end
|
50
49
|
end
|
51
50
|
|
51
|
+
# jump to character-aware position
|
52
|
+
# TODO: handle jump backward
|
53
|
+
def charpos=(new_charpos)
|
54
|
+
skip(/.{#{new_charpos - charpos}}/m)
|
55
|
+
end
|
56
|
+
|
52
57
|
private
|
53
58
|
|
54
59
|
def column_location(term)
|
55
60
|
ColumnLocation.new(
|
56
61
|
byte_offset: pos - term.bytesize,
|
57
62
|
char_offset: charpos - term.length,
|
58
|
-
line_location: line.location.line_location
|
63
|
+
**(line.respond_to?(:location) ? { line_location: line.location.line_location } : {})
|
59
64
|
)
|
60
65
|
end
|
61
66
|
|
@@ -64,23 +69,10 @@ module Spellr
|
|
64
69
|
end
|
65
70
|
|
66
71
|
def next_term
|
67
|
-
if skip_nonwords_and_flags
|
68
|
-
nil
|
69
|
-
else
|
70
|
-
scan_term
|
71
|
-
end
|
72
|
-
end
|
72
|
+
return if skip_nonwords_and_flags
|
73
73
|
|
74
|
-
|
75
|
-
|
76
|
-
# [WORD] [WORD]Word [WORDN'T] [WORD]'S [WORD]'s [WORD]s
|
77
|
-
UPPER_CASE_RE = /[[:upper:]]+(?:['’][[:upper:]]+(?<!['’][Ss]))*(?:(?![[:lower:]])|(?=s(?![[:lower:]])))/.freeze
|
78
|
-
# [word] [word]'s [wordn't]
|
79
|
-
LOWER_CASE_RE = /[[:lower:]]+(?:['’][[:lower:]]+(?<!['’]s))*/.freeze
|
80
|
-
# for characters in [:alpha:] that aren't in [:lower:] or [:upper:] e.g. Arabic
|
81
|
-
OTHER_CASE_RE = /(?:[[:alpha:]](?<![[:lower:][:upper:]]))+/.freeze
|
82
|
-
|
83
|
-
TERM_RE = Regexp.union(TITLE_CASE_RE, UPPER_CASE_RE, LOWER_CASE_RE, OTHER_CASE_RE)
|
74
|
+
scan_term
|
75
|
+
end
|
84
76
|
|
85
77
|
def scan_term
|
86
78
|
term = scan(TERM_RE)
|
@@ -88,126 +80,35 @@ module Spellr
|
|
88
80
|
return term if term && term.length >= Spellr.config.word_minimum_length
|
89
81
|
end
|
90
82
|
|
91
|
-
NOT_EVEN_NON_WORDS_RE = %r{[^[:alpha:]/%#0-9\\]+}.freeze # everything not covered by more specific skips/scans
|
92
|
-
LEFTOVER_NON_WORD_BITS_RE = %r{[/%#\\]|\d+}.freeze # e.g. a / not starting //a-url.com
|
93
|
-
HEX_RE = /(?:#(?:\h{6}|\h{3})|0x\h+)(?![[:alpha:]])/.freeze
|
94
|
-
SHELL_COLOR_ESCAPE_RE = /\\(?:e|0?33)\[\d+(;\d+)*m/.freeze
|
95
|
-
PUNYCODE_RE = /xn--[a-v0-9\-]+(?:[[:alpha:]])/.freeze
|
96
|
-
BACKSLASH_ESCAPE_RE = /\\[a-zA-Z]/.freeze # TODO: hex escapes e.g. \xAA. TODO: language aware escapes
|
97
|
-
REPEATED_SINGLE_LETTERS_RE = /(?:([[:alpha:]])\1+)(?![[:alpha:]])/.freeze # e.g. xxxxxxxx (it's not a word)
|
98
|
-
URL_ENCODED_ENTITIES_RE = /%[0-8A-F]{2}/.freeze
|
99
|
-
# There's got to be a better way of writing this
|
100
|
-
SEQUENTIAL_LETTERS_RE = /a(?:b(?:c(?:d(?:e(?:f(?:g(?:h(?:i(?:j(?:k(?:l(?:m(?:n(?:o(?:p(?:q(?:r(?:s(?:t(?:u(?:v(?:w(?:x(?:yz?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?(?![[:alpha:]])/i.freeze # rubocop:disable Metrics/LineLength
|
101
|
-
|
102
|
-
# I didn't want to do this myself. BUT i need something to heuristically match on, and it's difficult
|
103
|
-
URL_SCHEME = '(//|https?://|s?ftp://|mailto:)'
|
104
|
-
URL_USERINFO = '([[:alnum:]]+(?::[[:alnum:]]+)?@)'
|
105
|
-
URL_HOSTNAME = '((?:[[:alnum:]-]+(?:\\\\?\\.[[:alnum:]-]+)+|localhost|\\d{1,3}(?:\\.\\d{1,3}){3}))'
|
106
|
-
URL_PORT = '(:\\d+)'
|
107
|
-
URL_PATH = '(/(?:[[:alnum:]=@!$&\\-/._\\\\]|%\h{2})+)'
|
108
|
-
URL_QUERY = '(\\?(?:[[:alnum:]=!$\\-/.\\\\]|%\\h{2})+(?:&(?:[[:alnum:]=!$\\-/.\\\\]|%\\h{2})+)*)'
|
109
|
-
URL_FRAGMENT = '(\\#(?:[[:alnum:]=!$&\\-/.\\\\]|%\\h{2})+)'
|
110
|
-
URL_RE = /
|
111
|
-
(?:
|
112
|
-
#{URL_SCHEME}#{URL_USERINFO}?#{URL_HOSTNAME}#{URL_PORT}?#{URL_PATH}?
|
113
|
-
|
|
114
|
-
#{URL_SCHEME}?#{URL_USERINFO}#{URL_HOSTNAME}#{URL_PORT}?#{URL_PATH}?
|
115
|
-
|
|
116
|
-
#{URL_SCHEME}?#{URL_USERINFO}?#{URL_HOSTNAME}#{URL_PORT}?#{URL_PATH}
|
117
|
-
)
|
118
|
-
#{URL_QUERY}?#{URL_FRAGMENT}?
|
119
|
-
/x.freeze
|
120
|
-
|
121
|
-
KNOWN_KEY_PATTERNS_RE = %r{(
|
122
|
-
SG\.[\w\-]{22}\.[\w\-]{43} | # sendgrid
|
123
|
-
prg-\h{8}-\h{4}-\h{4}-\h{4}-\h{12} | # hyperwallet
|
124
|
-
GTM-[A-Z0-9]{7} | # google tag manager
|
125
|
-
sha1-[A-Za-z0-9=+/]{28} |
|
126
|
-
sha512-[A-Za-z0-9=+/]{88} |
|
127
|
-
data:[a-z/;0-9\-]+;base64,[A-Za-z0-9+/]+=*(?![[:alnum:]])
|
128
|
-
)}x.freeze
|
129
|
-
|
130
|
-
SKIPS = Regexp.union(
|
131
|
-
NOT_EVEN_NON_WORDS_RE,
|
132
|
-
SHELL_COLOR_ESCAPE_RE,
|
133
|
-
BACKSLASH_ESCAPE_RE,
|
134
|
-
URL_ENCODED_ENTITIES_RE,
|
135
|
-
HEX_RE,
|
136
|
-
URL_RE, # 2%
|
137
|
-
KNOWN_KEY_PATTERNS_RE
|
138
|
-
).freeze
|
139
|
-
|
140
|
-
AFTER_KEY_SKIPS = Regexp.union(
|
141
|
-
LEFTOVER_NON_WORD_BITS_RE,
|
142
|
-
REPEATED_SINGLE_LETTERS_RE,
|
143
|
-
SEQUENTIAL_LETTERS_RE
|
144
|
-
)
|
145
|
-
|
146
83
|
def skip_nonwords
|
147
|
-
skip(SKIPS) ||
|
148
|
-
skip_key_heuristically || # 5%
|
149
|
-
skip(AFTER_KEY_SKIPS)
|
84
|
+
skip(SKIPS) || skip_key_heuristically || skip(AFTER_KEY_SKIPS)
|
150
85
|
end
|
151
86
|
|
152
|
-
|
153
|
-
|
154
|
-
def skip_key_heuristically # rubocop:disable Metrics/MethodLength
|
155
|
-
return unless scan(KEY_RE)
|
156
|
-
# I've come across some large base64 strings by this point they're definitely base64.
|
157
|
-
return true if matched.length > 200
|
158
|
-
|
159
|
-
if key_roughly?(matched)
|
160
|
-
if N.key?(matched)
|
161
|
-
true
|
162
|
-
else
|
163
|
-
unscan
|
164
|
-
false
|
165
|
-
end
|
166
|
-
else
|
167
|
-
unscan
|
168
|
-
false
|
169
|
-
end
|
170
|
-
end
|
87
|
+
def skip_key_heuristically
|
88
|
+
possible_key = check(POSSIBLE_KEY_RE)
|
171
89
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
[A-Z][a-z]{#{Spellr.config.word_minimum_length - 1}}
|
177
|
-
|
|
178
|
-
[a-z]{#{Spellr.config.word_minimum_length}}
|
179
|
-
|
|
180
|
-
[A-Z]{#{Spellr.config.word_minimum_length}}
|
181
|
-
)/x.freeze
|
182
|
-
end
|
183
|
-
ALPHA_SEP_RE = '[A-Za-z][A-Za-z\\-_/+]*'
|
184
|
-
NUM_SEP_RE = '\\d[\\d\\-_/+]*'
|
185
|
-
THREE_CHUNK_RE = /^(?:
|
186
|
-
#{ALPHA_SEP_RE}#{NUM_SEP_RE}#{ALPHA_SEP_RE}
|
187
|
-
|
|
188
|
-
#{NUM_SEP_RE}#{ALPHA_SEP_RE}#{NUM_SEP_RE}
|
189
|
-
)/x.freeze
|
190
|
-
def key_roughly?(matched)
|
191
|
-
return unless matched.length >= Spellr.config.key_minimum_length
|
192
|
-
return unless matched.match?(THREE_CHUNK_RE)
|
193
|
-
return unless matched.match?(min_alpha_re) # or there's no point
|
194
|
-
|
195
|
-
true
|
90
|
+
return unless possible_key
|
91
|
+
return unless key?(possible_key)
|
92
|
+
|
93
|
+
self.pos += possible_key.bytesize
|
196
94
|
end
|
197
95
|
|
198
|
-
|
199
|
-
def
|
200
|
-
|
96
|
+
BAYES_KEY_HEURISTIC = NaiveBayes.new
|
97
|
+
def key?(possible_key)
|
98
|
+
# I've come across some large base64 strings by this point they're definitely base64.
|
99
|
+
return true if possible_key.length > 200
|
100
|
+
return unless possible_key.length >= Spellr.config.key_minimum_length
|
101
|
+
return unless possible_key.match?(min_alpha_re) # or there's no point
|
102
|
+
|
103
|
+
BAYES_KEY_HEURISTIC.key?(possible_key)
|
201
104
|
end
|
202
105
|
|
203
|
-
SPELLR_DISABLE_RE = /spellr:disable/.freeze
|
204
106
|
def skip_and_track_disable
|
205
107
|
return if disabled?
|
206
108
|
|
207
109
|
skip(SPELLR_DISABLE_RE) && self.disabled = true
|
208
110
|
end
|
209
111
|
|
210
|
-
SPELLR_ENABLE_RE = /spellr:enable/.freeze
|
211
112
|
def skip_and_track_enable
|
212
113
|
return unless disabled?
|
213
114
|
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spellr
|
4
|
+
class Output
|
5
|
+
attr_reader :exit_code
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@exit_code = 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def stdin
|
12
|
+
@stdin ||= $stdin
|
13
|
+
end
|
14
|
+
|
15
|
+
def stdout
|
16
|
+
@stdout ||= $stdout
|
17
|
+
end
|
18
|
+
|
19
|
+
def stderr
|
20
|
+
@stderr ||= $stderr
|
21
|
+
end
|
22
|
+
|
23
|
+
def stdout?
|
24
|
+
defined?(@stdout)
|
25
|
+
end
|
26
|
+
|
27
|
+
def stderr?
|
28
|
+
defined?(@stderr)
|
29
|
+
end
|
30
|
+
|
31
|
+
def counts
|
32
|
+
@counts ||= Hash.new(0)
|
33
|
+
end
|
34
|
+
|
35
|
+
def exit_code=(value)
|
36
|
+
@exit_code = value unless value.zero?
|
37
|
+
end
|
38
|
+
|
39
|
+
def increment(counter)
|
40
|
+
counts[counter] += 1
|
41
|
+
end
|
42
|
+
|
43
|
+
def puts(str)
|
44
|
+
stdout.puts(str)
|
45
|
+
end
|
46
|
+
|
47
|
+
def warn(str)
|
48
|
+
stderr.puts(str)
|
49
|
+
end
|
50
|
+
|
51
|
+
def print(str)
|
52
|
+
stdout.print(str)
|
53
|
+
end
|
54
|
+
|
55
|
+
def <<(other) # rubocop:disable Metrics/AbcSize
|
56
|
+
self.exit_code = other.exit_code
|
57
|
+
stderr.puts other.stderr.string if other.stderr?
|
58
|
+
stdout.puts other.stdout.string if other.stdout?
|
59
|
+
counts.merge!(other.counts) { |_k, a, b| a + b }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'output'
|
4
|
+
|
5
|
+
module Spellr
|
6
|
+
class OutputStubbed < Spellr::Output
|
7
|
+
attr_accessor :exit_code
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@exit_code = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def stdin
|
14
|
+
@stdin ||= StringIO.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def stdout
|
18
|
+
@stdout ||= StringIO.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def stderr
|
22
|
+
@stderr ||= StringIO.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def marshal_dump # rubocop:disable Metrics/MethodLength
|
26
|
+
{
|
27
|
+
exit_code: exit_code,
|
28
|
+
counts: @counts,
|
29
|
+
stdin: @stdin&.string,
|
30
|
+
stdin_pos: @stdin&.pos,
|
31
|
+
stdout: @stdout&.string,
|
32
|
+
stdout_pos: @stdout&.pos,
|
33
|
+
stderr: @stderr&.string,
|
34
|
+
stderr_pos: @stderr&.pos
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def marshal_load(dumped) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
39
|
+
if dumped[:stdin]
|
40
|
+
@stdin = StringIO.new(dumped[:stdin])
|
41
|
+
@stdin.pos = dumped[:stdin_pos]
|
42
|
+
end
|
43
|
+
|
44
|
+
if dumped[:stdout]
|
45
|
+
@stdout = StringIO.new(dumped[:stdout])
|
46
|
+
@stdout.pos = dumped[:stdout_pos]
|
47
|
+
end
|
48
|
+
|
49
|
+
if dumped[:stderr]
|
50
|
+
@stderr = StringIO.new(dumped[:stderr])
|
51
|
+
@stderr.pos = dumped[:stderr_pos]
|
52
|
+
end
|
53
|
+
|
54
|
+
@exit_code = dumped[:exit_code]
|
55
|
+
@counts = dumped[:counts]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/spellr/reporter.rb
CHANGED
@@ -1,27 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'base_reporter'
|
4
4
|
|
5
5
|
module Spellr
|
6
|
-
class Reporter
|
7
|
-
|
8
|
-
|
9
|
-
attr_accessor :total
|
10
|
-
|
11
|
-
def initialize
|
12
|
-
@total = 0
|
6
|
+
class Reporter < Spellr::BaseReporter
|
7
|
+
def parallel?
|
8
|
+
true
|
13
9
|
end
|
14
10
|
|
15
|
-
def finish
|
11
|
+
def finish
|
16
12
|
puts "\n"
|
17
|
-
puts "#{pluralize 'file', checked} checked"
|
18
|
-
puts "#{pluralize 'error', total} found"
|
13
|
+
puts "#{pluralize 'file', counts[:checked]} checked"
|
14
|
+
puts "#{pluralize 'error', counts[:total]} found"
|
19
15
|
end
|
20
16
|
|
21
17
|
def call(token)
|
22
|
-
|
18
|
+
super
|
23
19
|
|
24
|
-
|
20
|
+
increment(:total)
|
25
21
|
end
|
26
22
|
end
|
27
23
|
end
|
data/lib/spellr/token.rb
CHANGED
@@ -1,18 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# frozen string_literal: true
|
4
|
-
|
5
3
|
require_relative 'column_location'
|
6
4
|
require_relative 'string_format'
|
7
5
|
|
8
6
|
class String
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
def normalize_cache
|
14
|
-
@@normalize_cache ||= Hash.new do |cache, term| # rubocop:disable Style/ClassVars # i want this shared with subclasses
|
15
|
-
cache[term] = term.strip.downcase.unicode_normalize.tr('’', "'") + "\n"
|
7
|
+
def spellr_normalize
|
8
|
+
@@spellr_normalize ||= {} # rubocop:disable Style/ClassVars # I want to share this with subclasses
|
9
|
+
@@spellr_normalize.fetch(to_s) do |term|
|
10
|
+
@@spellr_normalize[term] = "#{term.strip.downcase.unicode_normalize.tr('’', "'")}\n"
|
16
11
|
end
|
17
12
|
end
|
18
13
|
end
|
@@ -49,6 +44,11 @@ module Spellr
|
|
49
44
|
)
|
50
45
|
end
|
51
46
|
|
47
|
+
def line=(new_line)
|
48
|
+
@line = new_line
|
49
|
+
location.line_location = new_line.location.line_location
|
50
|
+
end
|
51
|
+
|
52
52
|
def inspect
|
53
53
|
"#<#{self.class.name} #{to_s.inspect} @#{location}>"
|
54
54
|
end
|
@@ -61,6 +61,10 @@ module Spellr
|
|
61
61
|
@byte_range ||= location.byte_offset...(location.byte_offset + bytesize)
|
62
62
|
end
|
63
63
|
|
64
|
+
def file_char_range
|
65
|
+
@file_char_range ||= location.absolute_char_offset...(location.absolute_char_offset + length)
|
66
|
+
end
|
67
|
+
|
64
68
|
def coordinates
|
65
69
|
location.coordinates
|
66
70
|
end
|
@@ -71,13 +75,7 @@ module Spellr
|
|
71
75
|
|
72
76
|
def replace(replacement)
|
73
77
|
@replacement = replacement
|
74
|
-
|
75
|
-
body = f.read
|
76
|
-
body[location.absolute_char_offset...(location.absolute_char_offset + length)] = replacement
|
77
|
-
f.rewind
|
78
|
-
f.truncate(0)
|
79
|
-
f.write(body)
|
80
|
-
end
|
78
|
+
location.file.insert(replacement, file_char_range)
|
81
79
|
end
|
82
80
|
|
83
81
|
def file_name
|