goodcheck 2.6.1 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 :import_target, string
191
- let :imports, array(import_target)
192
- let :exclude, array_or(string)
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(imports),
197
- exclude: optional(exclude)
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
- Goodcheck.logger.tagged "#{path}" do
218
- Schema.config.coerce(content)
224
+ Schema.config.coerce(content)
219
225
 
220
- rules = []
226
+ rules = []
221
227
 
222
- load_rules(rules, array(content[:rules]))
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
- exclude_paths = Array(content[:exclude])
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
- Goodcheck.logger.tagged import do
244
- import_loader.load(import) do |content|
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
- Schema.rules.coerce json
248
- load_rules(rules, json)
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)
@@ -2,5 +2,7 @@ module Goodcheck
2
2
  module ExitStatus
3
3
  EXIT_SUCCESS = 0
4
4
  EXIT_ERROR = 1
5
+ EXIT_MATCH = 2
6
+ EXIT_TEST_FAILED = 3
5
7
  end
6
8
  end
@@ -18,6 +18,15 @@ 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
+ end
29
+
21
30
  attr_reader :cache_path
22
31
  attr_reader :expires_in
23
32
  attr_reader :force_download
@@ -31,9 +40,13 @@ module Goodcheck
31
40
  end
32
41
 
33
42
  def load(name, &block)
34
- uri = URI.parse(name)
43
+ uri = begin
44
+ URI.parse(name)
45
+ rescue URI::InvalidURIError
46
+ nil
47
+ end
35
48
 
36
- case uri.scheme
49
+ case uri&.scheme
37
50
  when nil
38
51
  load_file name, &block
39
52
  when "file"
@@ -45,14 +58,20 @@ module Goodcheck
45
58
  end
46
59
  end
47
60
 
48
- def load_file(path)
49
- files = Dir.glob(File.join(config_path.parent.to_path, path), File::FNM_DOTMATCH | File::FNM_EXTGLOB).sort
61
+ def load_file(path, &block)
62
+ files = Pathname.glob(File.join(config_path.parent.to_path, path), File::FNM_DOTMATCH | File::FNM_EXTGLOB).sort
50
63
  if files.empty?
51
64
  raise FileNotFound.new(path)
52
65
  else
53
66
  files.each do |file|
54
67
  Goodcheck.logger.info "Reading file: #{file}"
55
- yield File.read(file)
68
+ if unarchiver.tar_gz?(file)
69
+ unarchiver.tar_gz(file.read) do |content, filename|
70
+ block.call(content, filename)
71
+ end
72
+ else
73
+ block.call(file.read, file.to_path)
74
+ end
56
75
  end
57
76
  end
58
77
  end
@@ -61,7 +80,7 @@ module Goodcheck
61
80
  Digest::SHA2.hexdigest(uri.to_s)
62
81
  end
63
82
 
64
- def load_http(uri)
83
+ def load_http(uri, &block)
65
84
  hash = cache_name(uri)
66
85
  path = cache_path + hash
67
86
 
@@ -87,13 +106,19 @@ module Goodcheck
87
106
  if download
88
107
  path.rmtree if path.exist?
89
108
  Goodcheck.logger.info "Downloading content..."
90
- content = http_get uri
91
- Goodcheck.logger.debug "Downloaded content: #{content[0, 1024].inspect}#{content.size > 1024 ? "..." : ""}"
92
- yield content
93
- write_cache uri, content
109
+ if unarchiver.tar_gz?(uri.path)
110
+ unarchiver.tar_gz(http_get(uri)) do |content, filename|
111
+ block.call(content, filename)
112
+ write_cache "#{uri}/#{filename}", content
113
+ end
114
+ else
115
+ content = http_get(uri)
116
+ block.call(content, uri.path)
117
+ write_cache uri, content
118
+ end
94
119
  else
95
120
  Goodcheck.logger.info "Reading content from cache..."
96
- yield path.read
121
+ block.call(path.read, path.to_path)
97
122
  end
98
123
  end
99
124
 
@@ -106,16 +131,43 @@ module Goodcheck
106
131
  def http_get(uri, limit = 10)
107
132
  raise ArgumentError, "Too many HTTP redirects" if limit == 0
108
133
 
109
- res = Net::HTTP.get_response URI(uri)
110
- case res
111
- when Net::HTTPSuccess
112
- res.body
113
- when Net::HTTPRedirection
114
- location = res['Location']
115
- http_get location, limit - 1
116
- else
117
- raise "Error: HTTP GET #{uri.inspect} #{res.inspect}"
134
+ max_retry_count = 2
135
+ retry_count = 0
136
+ begin
137
+ res = Net::HTTP.get_response URI(uri)
138
+ case res
139
+ when Net::HTTPSuccess
140
+ res.body
141
+ when Net::HTTPRedirection
142
+ location = res['Location']
143
+ http_get location, limit - 1
144
+ when Net::HTTPClientError, Net::HTTPServerError
145
+ raise HTTPGetError.new(res)
146
+ else
147
+ raise Error, "HTTP GET failed due to #{res.inspect}"
148
+ end
149
+ rescue Net::OpenTimeout, HTTPGetError => exn
150
+ if retry_count < max_retry_count
151
+ retry_count += 1
152
+ Goodcheck.logger.info "Retry ##{retry_count} - HTTP GET #{uri} due to #{exn.inspect}..."
153
+ sleep 1
154
+ retry
155
+ else
156
+ raise
157
+ end
118
158
  end
119
159
  end
160
+
161
+ private
162
+
163
+ def unarchiver
164
+ @unarchiver ||=
165
+ begin
166
+ filter = ->(filename) {
167
+ %w[.yml .yaml].include?(File.extname(filename).downcase) && File.basename(filename) != DEFAULT_CONFIG_FILE
168
+ }
169
+ Unarchiver.new(file_filter: filter)
170
+ end
171
+ end
120
172
  end
121
173
  end
@@ -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:, range:, rule:, text:)
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
 
@@ -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
@@ -1,8 +1,8 @@
1
1
  module Goodcheck
2
2
  def self.logger
3
- @logger ||= ActiveSupport::TaggedLogging.new(Logger.new(STDERR)).tap do |logger|
4
- logger.push_tags VERSION
5
- logger.level = Logger::ERROR
6
- end
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