goodcheck 2.6.0 → 3.0.2
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 +41 -0
- data/LICENSE +1 -1
- data/README.md +8 -445
- data/lib/goodcheck.rb +6 -3
- data/lib/goodcheck/analyzer.rb +13 -9
- data/lib/goodcheck/buffer.rb +9 -21
- data/lib/goodcheck/cli.rb +6 -8
- data/lib/goodcheck/commands/check.rb +24 -28
- data/lib/goodcheck/commands/config_loading.rb +11 -5
- data/lib/goodcheck/commands/test.rb +35 -28
- data/lib/goodcheck/config.rb +68 -1
- data/lib/goodcheck/config_loader.rb +36 -27
- data/lib/goodcheck/exit_status.rb +2 -0
- data/lib/goodcheck/import_loader.rb +74 -20
- data/lib/goodcheck/issue.rb +3 -3
- data/lib/goodcheck/location.rb +28 -0
- data/lib/goodcheck/logger.rb +4 -4
- data/lib/goodcheck/reporters/json.rb +6 -1
- data/lib/goodcheck/reporters/text.rb +44 -11
- data/lib/goodcheck/rule.rb +3 -1
- data/lib/goodcheck/unarchiver.rb +40 -0
- data/lib/goodcheck/version.rb +1 -1
- metadata +42 -30
data/lib/goodcheck/config.rb
CHANGED
@@ -1,11 +1,43 @@
|
|
1
1
|
module Goodcheck
|
2
2
|
class Config
|
3
|
+
DEFAULT_EXCLUDE_BINARY = false
|
4
|
+
|
5
|
+
# https://www.iana.org/assignments/media-types/media-types.xhtml
|
6
|
+
BINARY_MIME_TYPES = %w[
|
7
|
+
audio
|
8
|
+
font
|
9
|
+
image
|
10
|
+
model
|
11
|
+
multipart
|
12
|
+
video
|
13
|
+
].to_set.freeze
|
14
|
+
BINARY_MIME_FULLTYPES = %w[
|
15
|
+
application/gzip
|
16
|
+
application/illustrator
|
17
|
+
application/pdf
|
18
|
+
application/zip
|
19
|
+
].to_set.freeze
|
20
|
+
|
3
21
|
attr_reader :rules
|
4
22
|
attr_reader :exclude_paths
|
23
|
+
attr_reader :exclude_binary
|
24
|
+
alias exclude_binary? exclude_binary
|
25
|
+
attr_reader :allowed_severities
|
26
|
+
attr_reader :severity_required
|
27
|
+
alias severity_required? severity_required
|
5
28
|
|
6
|
-
def initialize(rules:, exclude_paths:)
|
29
|
+
def initialize(rules:, exclude_paths:, exclude_binary: DEFAULT_EXCLUDE_BINARY, severity: nil)
|
7
30
|
@rules = rules
|
8
31
|
@exclude_paths = exclude_paths
|
32
|
+
@exclude_binary = exclude_binary || DEFAULT_EXCLUDE_BINARY
|
33
|
+
severity ||= {}
|
34
|
+
@allowed_severities = Set.new(severity.fetch(:allow, []))
|
35
|
+
@severity_required = severity.fetch(:required, false)
|
36
|
+
end
|
37
|
+
|
38
|
+
def severity_allowed?(severity)
|
39
|
+
return true if allowed_severities.empty?
|
40
|
+
allowed_severities.include?(severity)
|
9
41
|
end
|
10
42
|
|
11
43
|
def each_rule(filter:, &block)
|
@@ -44,5 +76,40 @@ module Goodcheck
|
|
44
76
|
enum_for(:rules_for_path, path, rules_filter: rules_filter)
|
45
77
|
end
|
46
78
|
end
|
79
|
+
|
80
|
+
def exclude_path?(path)
|
81
|
+
excluded = exclude_paths.any? do |pattern|
|
82
|
+
path.fnmatch?(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
83
|
+
end
|
84
|
+
|
85
|
+
return true if excluded
|
86
|
+
return excluded unless exclude_binary?
|
87
|
+
return excluded unless path.file?
|
88
|
+
|
89
|
+
exclude_file_by_mime_type?(path)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def exclude_file_by_mime_type?(file)
|
95
|
+
# NOTE: Lazy load to save memory
|
96
|
+
require "marcel"
|
97
|
+
|
98
|
+
fulltype = Marcel::MimeType.for(file)
|
99
|
+
type, subtype = fulltype.split("/")
|
100
|
+
|
101
|
+
case
|
102
|
+
when subtype.end_with?("+xml") # e.g. "image/svg+xml"
|
103
|
+
false
|
104
|
+
when BINARY_MIME_TYPES.include?(type)
|
105
|
+
Goodcheck.logger.debug "Exclude file: #{file} (#{fulltype})"
|
106
|
+
true
|
107
|
+
when BINARY_MIME_FULLTYPES.include?(fulltype)
|
108
|
+
Goodcheck.logger.debug "Exclude file: #{file} (#{fulltype})"
|
109
|
+
true
|
110
|
+
else
|
111
|
+
false
|
112
|
+
end
|
113
|
+
end
|
47
114
|
end
|
48
115
|
end
|
@@ -103,7 +103,8 @@ module Goodcheck
|
|
103
103
|
justification: optional(array_or(string)),
|
104
104
|
glob: optional(glob),
|
105
105
|
pass: optional(array_or(string)),
|
106
|
-
fail: optional(array_or(string))
|
106
|
+
fail: optional(array_or(string)),
|
107
|
+
severity: optional(string)
|
107
108
|
)
|
108
109
|
|
109
110
|
let :negative_rule, object(
|
@@ -113,14 +114,16 @@ module Goodcheck
|
|
113
114
|
justification: optional(array_or(string)),
|
114
115
|
glob: optional(glob),
|
115
116
|
pass: optional(array_or(string)),
|
116
|
-
fail: optional(array_or(string))
|
117
|
+
fail: optional(array_or(string)),
|
118
|
+
severity: optional(string)
|
117
119
|
)
|
118
120
|
|
119
121
|
let :nopattern_rule, object(
|
120
122
|
id: string,
|
121
123
|
message: string,
|
122
124
|
justification: optional(array_or(string)),
|
123
|
-
glob: glob
|
125
|
+
glob: glob,
|
126
|
+
severity: optional(string)
|
124
127
|
)
|
125
128
|
|
126
129
|
let :positive_trigger, object(
|
@@ -163,7 +166,8 @@ module Goodcheck
|
|
163
166
|
id: string,
|
164
167
|
message: string,
|
165
168
|
justification: optional(array_or(string)),
|
166
|
-
trigger: array_or(trigger)
|
169
|
+
trigger: array_or(trigger),
|
170
|
+
severity: optional(string)
|
167
171
|
)
|
168
172
|
|
169
173
|
let :rule, enum(positive_rule,
|
@@ -187,14 +191,17 @@ module Goodcheck
|
|
187
191
|
|
188
192
|
let :rules, array(rule)
|
189
193
|
|
190
|
-
let :
|
191
|
-
|
192
|
-
|
194
|
+
let :severity, object(
|
195
|
+
allow: optional(array(string)),
|
196
|
+
required: boolean?
|
197
|
+
)
|
193
198
|
|
194
199
|
let :config, object(
|
195
200
|
rules: optional(rules),
|
196
|
-
import: optional(
|
197
|
-
exclude: optional(
|
201
|
+
import: optional(array(string)),
|
202
|
+
exclude: optional(array_or(string)),
|
203
|
+
exclude_binary: boolean?,
|
204
|
+
severity: optional(severity)
|
198
205
|
)
|
199
206
|
end
|
200
207
|
|
@@ -214,39 +221,40 @@ module Goodcheck
|
|
214
221
|
|
215
222
|
def load
|
216
223
|
Goodcheck.logger.info "Loading configuration: #{path}"
|
217
|
-
|
218
|
-
Schema.config.coerce(content)
|
224
|
+
Schema.config.coerce(content)
|
219
225
|
|
220
|
-
|
226
|
+
rules = []
|
221
227
|
|
222
|
-
|
223
|
-
|
224
|
-
Array(content[:import]).each do |import|
|
225
|
-
load_import rules, import
|
226
|
-
end
|
228
|
+
load_rules(rules, array(content[:rules]))
|
227
229
|
|
228
|
-
|
229
|
-
|
230
|
-
Config.new(rules: rules, exclude_paths: exclude_paths)
|
230
|
+
Array(content[:import]).each do |import|
|
231
|
+
load_import rules, import
|
231
232
|
end
|
233
|
+
|
234
|
+
Config.new(
|
235
|
+
rules: rules,
|
236
|
+
exclude_paths: Array(content[:exclude]),
|
237
|
+
exclude_binary: content[:exclude_binary],
|
238
|
+
severity: content[:severity]
|
239
|
+
)
|
232
240
|
end
|
233
241
|
|
234
242
|
def load_rules(rules, array)
|
235
243
|
array.each do |hash|
|
236
244
|
rules << load_rule(hash)
|
245
|
+
rescue RegexpError => exn
|
246
|
+
raise InvalidPattern, "Invalid pattern of the `#{hash.fetch(:id)}` rule in `#{path}`: #{exn.message}"
|
237
247
|
end
|
238
248
|
end
|
239
249
|
|
240
250
|
def load_import(rules, import)
|
241
251
|
Goodcheck.logger.info "Importing rules from #{import}"
|
242
252
|
|
243
|
-
|
244
|
-
|
245
|
-
json = JSON.parse(JSON.dump(YAML.load(content, filename: import)), symbolize_names: true)
|
253
|
+
import_loader.load(import) do |content, filename|
|
254
|
+
json = JSON.parse(JSON.dump(YAML.safe_load(content, filename: filename)), symbolize_names: true)
|
246
255
|
|
247
|
-
|
248
|
-
|
249
|
-
end
|
256
|
+
Schema.rules.coerce json
|
257
|
+
load_rules(rules, json)
|
250
258
|
end
|
251
259
|
end
|
252
260
|
|
@@ -257,8 +265,9 @@ module Goodcheck
|
|
257
265
|
triggers = retrieve_triggers(hash)
|
258
266
|
justifications = array(hash[:justification])
|
259
267
|
message = hash[:message].chomp
|
268
|
+
severity = hash[:severity]
|
260
269
|
|
261
|
-
Rule.new(id: id, message: message, justifications: justifications, triggers: triggers)
|
270
|
+
Rule.new(id: id, message: message, justifications: justifications, triggers: triggers, severity: severity)
|
262
271
|
end
|
263
272
|
|
264
273
|
def retrieve_triggers(hash)
|
@@ -18,6 +18,19 @@ module Goodcheck
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
class HTTPGetError < Error
|
22
|
+
attr_reader :response
|
23
|
+
|
24
|
+
def initialize(res)
|
25
|
+
super("HTTP GET #{res.uri} => #{res.code} #{res.message}")
|
26
|
+
@response = res
|
27
|
+
end
|
28
|
+
|
29
|
+
def error_response?
|
30
|
+
response.is_a?(Net::HTTPClientError) || response.is_a?(Net::HTTPServerError)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
21
34
|
attr_reader :cache_path
|
22
35
|
attr_reader :expires_in
|
23
36
|
attr_reader :force_download
|
@@ -31,9 +44,13 @@ module Goodcheck
|
|
31
44
|
end
|
32
45
|
|
33
46
|
def load(name, &block)
|
34
|
-
uri =
|
47
|
+
uri = begin
|
48
|
+
URI.parse(name)
|
49
|
+
rescue URI::InvalidURIError
|
50
|
+
nil
|
51
|
+
end
|
35
52
|
|
36
|
-
case uri
|
53
|
+
case uri&.scheme
|
37
54
|
when nil
|
38
55
|
load_file name, &block
|
39
56
|
when "file"
|
@@ -45,14 +62,20 @@ module Goodcheck
|
|
45
62
|
end
|
46
63
|
end
|
47
64
|
|
48
|
-
def load_file(path)
|
49
|
-
files =
|
65
|
+
def load_file(path, &block)
|
66
|
+
files = Pathname.glob(File.join(config_path.parent.to_path, path), File::FNM_DOTMATCH | File::FNM_EXTGLOB).sort
|
50
67
|
if files.empty?
|
51
68
|
raise FileNotFound.new(path)
|
52
69
|
else
|
53
70
|
files.each do |file|
|
54
71
|
Goodcheck.logger.info "Reading file: #{file}"
|
55
|
-
|
72
|
+
if unarchiver.tar_gz?(file)
|
73
|
+
unarchiver.tar_gz(file.read) do |content, filename|
|
74
|
+
block.call(content, filename)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
block.call(file.read, file.to_path)
|
78
|
+
end
|
56
79
|
end
|
57
80
|
end
|
58
81
|
end
|
@@ -61,7 +84,7 @@ module Goodcheck
|
|
61
84
|
Digest::SHA2.hexdigest(uri.to_s)
|
62
85
|
end
|
63
86
|
|
64
|
-
def load_http(uri)
|
87
|
+
def load_http(uri, &block)
|
65
88
|
hash = cache_name(uri)
|
66
89
|
path = cache_path + hash
|
67
90
|
|
@@ -87,13 +110,19 @@ module Goodcheck
|
|
87
110
|
if download
|
88
111
|
path.rmtree if path.exist?
|
89
112
|
Goodcheck.logger.info "Downloading content..."
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
113
|
+
if unarchiver.tar_gz?(uri.path)
|
114
|
+
unarchiver.tar_gz(http_get(uri)) do |content, filename|
|
115
|
+
block.call(content, filename)
|
116
|
+
write_cache "#{uri}/#{filename}", content
|
117
|
+
end
|
118
|
+
else
|
119
|
+
content = http_get(uri)
|
120
|
+
block.call(content, uri.path)
|
121
|
+
write_cache uri, content
|
122
|
+
end
|
94
123
|
else
|
95
124
|
Goodcheck.logger.info "Reading content from cache..."
|
96
|
-
|
125
|
+
block.call(path.read, path.to_path)
|
97
126
|
end
|
98
127
|
end
|
99
128
|
|
@@ -106,16 +135,41 @@ module Goodcheck
|
|
106
135
|
def http_get(uri, limit = 10)
|
107
136
|
raise ArgumentError, "Too many HTTP redirects" if limit == 0
|
108
137
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
res.
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
138
|
+
max_retry_count = 2
|
139
|
+
retry_count = 0
|
140
|
+
begin
|
141
|
+
res = Net::HTTP.get_response URI(uri)
|
142
|
+
case res
|
143
|
+
when Net::HTTPSuccess
|
144
|
+
res.body
|
145
|
+
when Net::HTTPRedirection
|
146
|
+
location = res['Location']
|
147
|
+
http_get location, limit - 1
|
148
|
+
else
|
149
|
+
raise HTTPGetError.new(res)
|
150
|
+
end
|
151
|
+
rescue Net::OpenTimeout, HTTPGetError => exn
|
152
|
+
if retry_count < max_retry_count && exn.error_response?
|
153
|
+
retry_count += 1
|
154
|
+
Goodcheck.logger.info "#{retry_count} retry HTTP GET #{exn.response.uri} due to '#{exn.response.code} #{exn.response.message}'..."
|
155
|
+
sleep 1
|
156
|
+
retry
|
157
|
+
else
|
158
|
+
raise
|
159
|
+
end
|
118
160
|
end
|
119
161
|
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def unarchiver
|
166
|
+
@unarchiver ||=
|
167
|
+
begin
|
168
|
+
filter = ->(filename) {
|
169
|
+
%w[.yml .yaml].include?(File.extname(filename).downcase) && File.basename(filename) != DEFAULT_CONFIG_FILE
|
170
|
+
}
|
171
|
+
Unarchiver.new(file_filter: filter)
|
172
|
+
end
|
173
|
+
end
|
120
174
|
end
|
121
175
|
end
|
data/lib/goodcheck/issue.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
module Goodcheck
|
2
2
|
class Issue
|
3
3
|
attr_reader :buffer
|
4
|
-
attr_reader :range
|
5
4
|
attr_reader :rule
|
6
5
|
attr_reader :text
|
6
|
+
attr_reader :range
|
7
7
|
|
8
|
-
def initialize(buffer:,
|
8
|
+
def initialize(buffer:, rule:, text: nil, text_begin_pos: nil)
|
9
9
|
@buffer = buffer
|
10
|
-
@range = range
|
11
10
|
@rule = rule
|
12
11
|
@text = text
|
12
|
+
@range = text ? text_begin_pos..(text_begin_pos + text.bytesize - 1) : nil
|
13
13
|
@location = nil
|
14
14
|
end
|
15
15
|
|
data/lib/goodcheck/location.rb
CHANGED
@@ -1,4 +1,18 @@
|
|
1
1
|
module Goodcheck
|
2
|
+
# In the example below, each attribute is:
|
3
|
+
#
|
4
|
+
# - start_line: 2
|
5
|
+
# - start_column: 3
|
6
|
+
# - end_line: 2
|
7
|
+
# - end_column: 9
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
#
|
11
|
+
# 1 |
|
12
|
+
# 2 | A matched text
|
13
|
+
# 3 | ^~~~~~~
|
14
|
+
# 3456789
|
15
|
+
#
|
2
16
|
class Location
|
3
17
|
attr_reader :start_line
|
4
18
|
attr_reader :start_column
|
@@ -12,6 +26,14 @@ module Goodcheck
|
|
12
26
|
@end_column = end_column
|
13
27
|
end
|
14
28
|
|
29
|
+
def one_line?
|
30
|
+
start_line == end_line
|
31
|
+
end
|
32
|
+
|
33
|
+
def column_size
|
34
|
+
end_column - start_column + 1
|
35
|
+
end
|
36
|
+
|
15
37
|
def ==(other)
|
16
38
|
other.is_a?(Location) &&
|
17
39
|
other.start_line == start_line &&
|
@@ -19,5 +41,11 @@ module Goodcheck
|
|
19
41
|
other.end_line == end_line &&
|
20
42
|
other.end_column == end_column
|
21
43
|
end
|
44
|
+
|
45
|
+
alias eql? ==
|
46
|
+
|
47
|
+
def hash
|
48
|
+
self.class.hash ^ start_line.hash ^ start_column.hash ^ end_line.hash ^ end_column.hash
|
49
|
+
end
|
22
50
|
end
|
23
51
|
end
|
data/lib/goodcheck/logger.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Goodcheck
|
2
2
|
def self.logger
|
3
|
-
@logger ||=
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
@logger ||= Logger.new(
|
4
|
+
STDERR, level: Logger::ERROR,
|
5
|
+
formatter: ->(severity, time, progname, msg) { "[#{severity}] #{msg}\n" }
|
6
|
+
)
|
7
7
|
end
|
8
8
|
end
|
@@ -26,7 +26,8 @@ module Goodcheck
|
|
26
26
|
end_column: location.end_column
|
27
27
|
},
|
28
28
|
message: issue.rule.message,
|
29
|
-
justifications: issue.rule.justifications
|
29
|
+
justifications: issue.rule.justifications,
|
30
|
+
severity: issue.rule.severity
|
30
31
|
}
|
31
32
|
end
|
32
33
|
stdout.puts ::JSON.dump(json)
|
@@ -44,6 +45,10 @@ module Goodcheck
|
|
44
45
|
def issue(issue)
|
45
46
|
issues << issue
|
46
47
|
end
|
48
|
+
|
49
|
+
def summary
|
50
|
+
# noop
|
51
|
+
end
|
47
52
|
end
|
48
53
|
end
|
49
54
|
end
|