spellr 0.5.0 → 0.5.1
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 +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
|