goodcheck 2.5.2 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -3
  3. data/LICENSE +1 -1
  4. data/README.md +8 -445
  5. data/lib/goodcheck.rb +9 -4
  6. data/lib/goodcheck/analyzer.rb +13 -9
  7. data/lib/goodcheck/buffer.rb +9 -21
  8. data/lib/goodcheck/cli.rb +79 -57
  9. data/lib/goodcheck/commands/check.rb +41 -27
  10. data/lib/goodcheck/commands/config_loading.rb +28 -5
  11. data/lib/goodcheck/commands/init.rb +4 -2
  12. data/lib/goodcheck/commands/pattern.rb +2 -1
  13. data/lib/goodcheck/commands/test.rb +38 -30
  14. data/lib/goodcheck/config.rb +68 -1
  15. data/lib/goodcheck/config_loader.rb +41 -31
  16. data/lib/goodcheck/error.rb +3 -0
  17. data/lib/goodcheck/exit_status.rb +8 -0
  18. data/lib/goodcheck/glob.rb +14 -3
  19. data/lib/goodcheck/import_loader.rb +96 -26
  20. data/lib/goodcheck/issue.rb +3 -3
  21. data/lib/goodcheck/location.rb +28 -0
  22. data/lib/goodcheck/logger.rb +4 -4
  23. data/lib/goodcheck/reporters/json.rb +6 -1
  24. data/lib/goodcheck/reporters/text.rb +44 -11
  25. data/lib/goodcheck/rule.rb +3 -1
  26. data/lib/goodcheck/unarchiver.rb +40 -0
  27. data/lib/goodcheck/version.rb +1 -1
  28. metadata +44 -82
  29. data/.github/dependabot.yml +0 -18
  30. data/.github/workflows/release.yml +0 -16
  31. data/.github/workflows/test.yml +0 -46
  32. data/.gitignore +0 -13
  33. data/.rubocop.yml +0 -5
  34. data/Dockerfile +0 -13
  35. data/Gemfile +0 -6
  36. data/Rakefile +0 -75
  37. data/bin/console +0 -14
  38. data/bin/setup +0 -8
  39. data/cheatsheet.pdf +0 -0
  40. data/docusaurus/.dockerignore +0 -2
  41. data/docusaurus/.gitignore +0 -12
  42. data/docusaurus/Dockerfile +0 -10
  43. data/docusaurus/docker-compose.yml +0 -18
  44. data/docusaurus/docs/commands.md +0 -69
  45. data/docusaurus/docs/configuration.md +0 -300
  46. data/docusaurus/docs/development.md +0 -15
  47. data/docusaurus/docs/getstarted.md +0 -46
  48. data/docusaurus/docs/rules.md +0 -79
  49. data/docusaurus/website/README.md +0 -193
  50. data/docusaurus/website/core/Footer.js +0 -100
  51. data/docusaurus/website/package.json +0 -14
  52. data/docusaurus/website/pages/en/index.js +0 -207
  53. data/docusaurus/website/pages/en/versions.js +0 -118
  54. data/docusaurus/website/sidebars.json +0 -11
  55. data/docusaurus/website/siteConfig.js +0 -171
  56. data/docusaurus/website/static/css/code-block-buttons.css +0 -39
  57. data/docusaurus/website/static/css/custom.css +0 -245
  58. data/docusaurus/website/static/img/favicon.ico +0 -0
  59. data/docusaurus/website/static/js/code-block-buttons.js +0 -47
  60. data/docusaurus/website/versioned_docs/version-1.0.0/commands.md +0 -70
  61. data/docusaurus/website/versioned_docs/version-1.0.0/configuration.md +0 -296
  62. data/docusaurus/website/versioned_docs/version-1.0.0/development.md +0 -16
  63. data/docusaurus/website/versioned_docs/version-1.0.0/getstarted.md +0 -47
  64. data/docusaurus/website/versioned_docs/version-1.0.0/rules.md +0 -81
  65. data/docusaurus/website/versioned_docs/version-1.0.2/rules.md +0 -79
  66. data/docusaurus/website/versioned_docs/version-2.4.0/configuration.md +0 -301
  67. data/docusaurus/website/versioned_docs/version-2.4.3/rules.md +0 -80
  68. data/docusaurus/website/versioned_sidebars/version-1.0.0-sidebars.json +0 -11
  69. data/docusaurus/website/versioned_sidebars/version-1.0.2-sidebars.json +0 -11
  70. data/docusaurus/website/versioned_sidebars/version-2.4.0-sidebars.json +0 -11
  71. data/docusaurus/website/versions.json +0 -12
  72. data/docusaurus/website/yarn.lock +0 -6604
  73. data/goodcheck.gemspec +0 -35
  74. data/goodcheck.yml +0 -10
  75. data/logo/GoodCheck Horizontal.pdf +0 -899
  76. data/logo/GoodCheck Horizontal.png +0 -0
  77. data/logo/GoodCheck Horizontal.svg +0 -55
  78. data/logo/GoodCheck logo.png +0 -0
  79. data/logo/GoodCheck vertical.png +0 -0
  80. data/sample.yml +0 -57
@@ -1,11 +1,29 @@
1
1
  module Goodcheck
2
2
  module Commands
3
3
  module ConfigLoading
4
+ include ExitStatus
5
+
6
+ class ConfigFileNotFound < Error
7
+ attr_reader :path
8
+
9
+ def initialize(path:)
10
+ super(path.to_s)
11
+ @path = path
12
+ end
13
+ end
14
+
4
15
  attr_reader :config
5
16
 
6
17
  def load_config!(force_download:, cache_path:)
18
+ config_content =
19
+ begin
20
+ config_path.read
21
+ rescue Errno::ENOENT
22
+ raise ConfigFileNotFound.new(path: config_path)
23
+ end
24
+
7
25
  import_loader = ImportLoader.new(cache_path: cache_path, force_download: force_download, config_path: config_path)
8
- content = JSON.parse(JSON.dump(YAML.load(config_path.read, filename: config_path.to_s)), symbolize_names: true)
26
+ content = JSON.parse(JSON.dump(YAML.safe_load(config_content, filename: config_path.to_s)), symbolize_names: true)
9
27
  loader = ConfigLoader.new(path: config_path, content: content, stderr: stderr, import_loader: import_loader)
10
28
  @config = loader.load
11
29
  end
@@ -13,20 +31,25 @@ module Goodcheck
13
31
  def handle_config_errors(stderr)
14
32
  begin
15
33
  yield
16
-
34
+ rescue ConfigFileNotFound => exn
35
+ stderr.puts "Configuration file not found: #{exn.path}"
36
+ EXIT_ERROR
37
+ rescue ConfigLoader::InvalidPattern => exn
38
+ stderr.puts exn.message
39
+ EXIT_ERROR
17
40
  rescue Psych::Exception => exn
18
41
  stderr.puts "Unexpected error happens while loading YAML file: #{exn.inspect}"
19
42
  exn.backtrace.each do |trace_loc|
20
43
  stderr.puts " #{trace_loc}"
21
44
  end
22
- 1
45
+ EXIT_ERROR
23
46
  rescue StrongJSON::Type::TypeError, StrongJSON::Type::UnexpectedAttributeError => exn
24
47
  stderr.puts "Invalid config: #{exn.message}"
25
48
  stderr.puts StrongJSON::ErrorReporter.new(path: exn.path).to_s
26
- 1
49
+ EXIT_ERROR
27
50
  rescue Errno::ENOENT => exn
28
51
  stderr.puts "#{exn}"
29
- 1
52
+ EXIT_ERROR
30
53
  end
31
54
  end
32
55
  end
@@ -58,6 +58,8 @@ rules:
58
58
  # - vendor
59
59
  EOC
60
60
 
61
+ include ExitStatus
62
+
61
63
  attr_reader :stdout
62
64
  attr_reader :stderr
63
65
  attr_reader :path
@@ -73,7 +75,7 @@ rules:
73
75
  def run
74
76
  if path.file? && !force
75
77
  stderr.puts "#{path} already exists. Try --force option to overwrite the file."
76
- return 1
78
+ return EXIT_ERROR
77
79
  end
78
80
 
79
81
  path.open("w") do |io|
@@ -82,7 +84,7 @@ rules:
82
84
 
83
85
  stdout.puts "Wrote #{path}. ✍️"
84
86
 
85
- 0
87
+ EXIT_SUCCESS
86
88
  end
87
89
  end
88
90
  end
@@ -9,6 +9,7 @@ module Goodcheck
9
9
 
10
10
  include ConfigLoading
11
11
  include HomePath
12
+ include ExitStatus
12
13
 
13
14
  def initialize(stdout:, stderr:, path:, ids:, home_path:)
14
15
  @stdout = stdout
@@ -34,7 +35,7 @@ module Goodcheck
34
35
  end
35
36
  end
36
37
 
37
- 0
38
+ EXIT_SUCCESS
38
39
  end
39
40
  end
40
41
  end
@@ -3,6 +3,7 @@ module Goodcheck
3
3
  class Test
4
4
  include ConfigLoading
5
5
  include HomePath
6
+ include ExitStatus
6
7
 
7
8
  attr_reader :stdout
8
9
  attr_reader :stderr
@@ -24,18 +25,18 @@ module Goodcheck
24
25
 
25
26
  if config.rules.empty?
26
27
  stdout.puts "No rules."
27
- return 0
28
+ return EXIT_SUCCESS
28
29
  end
29
30
 
30
- validate_rule_uniqueness or return 1
31
- validate_rules or return 1
31
+ validate_rule_uniqueness or return EXIT_TEST_FAILED
32
+ validate_rules or return EXIT_TEST_FAILED
32
33
 
33
- 0
34
+ EXIT_SUCCESS
34
35
  end
35
36
  end
36
37
 
37
38
  def validate_rule_uniqueness
38
- stdout.puts "Validating rule id uniqueness..."
39
+ stdout.puts "Validating rule ID uniqueness..."
39
40
 
40
41
  duplicated_ids = []
41
42
 
@@ -46,36 +47,35 @@ module Goodcheck
46
47
  end
47
48
 
48
49
  if duplicated_ids.empty?
49
- stdout.puts " OK!👍"
50
+ stdout.puts Rainbow(" OK! 👍").green
50
51
  true
51
52
  else
52
53
  count = duplicated_ids.size
53
- stdout.puts(Rainbow(" Found #{count} #{'duplication'.pluralize(count)}.😞").red)
54
+ duplication = count == 1 ? 'duplication' : 'duplications'
55
+ stdout.puts " Found #{Rainbow(count).bold} #{duplication}. 😱"
54
56
  duplicated_ids.each do |id|
55
- stdout.puts " #{id}"
57
+ stdout.puts " - #{Rainbow(id).background(:red)}"
56
58
  end
57
59
  false
58
60
  end
59
61
  end
60
62
 
61
63
  def validate_rules
62
- test_pass = true
63
64
  success_count = 0
64
- failure_count = 0
65
65
  failed_rule_ids = Set[]
66
66
 
67
67
  config.rules.each do |rule|
68
- if rule.triggers.any? {|trigger| !trigger.passes.empty? || !trigger.fails.empty?}
69
- stdout.puts "Testing rule #{Rainbow(rule.id).cyan}..."
68
+ stdout.puts "Testing rule #{Rainbow(rule.id).cyan}..."
70
69
 
71
- rule_ok = true
70
+ rule_ok = true
72
71
 
72
+ if rule.triggers.any? {|trigger| !trigger.passes.empty? || !trigger.fails.empty?}
73
73
  rule.triggers.each.with_index do |trigger, index|
74
74
  if !trigger.passes.empty? || !trigger.fails.empty?
75
75
  if trigger.by_pattern?
76
76
  stdout.puts " Testing pattern..."
77
77
  else
78
- stdout.puts " Testing #{(index+1).ordinalize} trigger..."
78
+ stdout.puts " #{index + 1}. Testing trigger..."
79
79
  end
80
80
 
81
81
  pass_errors = trigger.passes.each.with_index.select do |pass, _|
@@ -87,21 +87,19 @@ module Goodcheck
87
87
  end
88
88
 
89
89
  unless pass_errors.empty?
90
- test_pass = false
91
90
  rule_ok = false
92
91
 
93
92
  pass_errors.each do |_, index|
94
- stdout.puts " #{(index+1).ordinalize} #{Rainbow('pass').green} example matched.😱"
93
+ stdout.puts " #{index + 1}. #{Rainbow('pass').green} example matched. 😱"
95
94
  failed_rule_ids << rule.id
96
95
  end
97
96
  end
98
97
 
99
98
  unless fail_errors.empty?
100
- test_pass = false
101
99
  rule_ok = false
102
100
 
103
101
  fail_errors.each do |_, index|
104
- stdout.puts " #{(index+1).ordinalize} #{Rainbow('fail').red} example didn't match.😱"
102
+ stdout.puts " #{index + 1}. #{Rainbow('fail').red} example didnt match. 😱"
105
103
  failed_rule_ids << rule.id
106
104
  end
107
105
  end
@@ -109,16 +107,27 @@ module Goodcheck
109
107
  end
110
108
 
111
109
  if rule.triggers.any?(&:skips_fail_examples?)
112
- stdout.puts " 🚨 The rule contains a `pattern` with `glob`, which is not supported by the test command."
110
+ stdout.puts " The rule contains a `pattern` with `glob`, which is not supported by the test command. 🚨"
113
111
  stdout.puts " Skips testing `fail` examples."
114
112
  end
113
+ end
115
114
 
116
- if rule_ok
117
- stdout.puts " OK!🎉"
118
- success_count += 1
119
- else
120
- failure_count += 1
121
- end
115
+ if rule.severity && !config.severity_allowed?(rule.severity)
116
+ allowed_severities = config.allowed_severities.map { |s| %("#{s}") }.join(', ')
117
+ stdout.puts Rainbow(" \"#{rule.severity}\" severity isn’t allowed. Must be one of #{allowed_severities}. 😱").red
118
+ rule_ok = false
119
+ failed_rule_ids << rule.id
120
+ end
121
+
122
+ if !rule.severity && config.severity_required?
123
+ stdout.puts Rainbow(" Severity is required. 😱").red
124
+ rule_ok = false
125
+ failed_rule_ids << rule.id
126
+ end
127
+
128
+ if rule_ok
129
+ stdout.puts Rainbow(" OK! 👍").green
130
+ success_count += 1
122
131
  end
123
132
  end
124
133
 
@@ -130,13 +139,12 @@ module Goodcheck
130
139
  end
131
140
  end
132
141
 
133
- rule_count = success_count + failure_count
142
+ total = success_count + failed_rule_ids.size
134
143
  stdout.puts ""
135
- stdout.puts ["Tested #{rule_count} #{'rule'.pluralize(rule_count)}",
136
- Rainbow("#{success_count} #{'success'.pluralize(success_count)}").green,
137
- Rainbow("#{failure_count} #{'failure'.pluralize(failure_count)}").red].join(", ")
144
+ stdout.puts "#{Rainbow(total).bold} #{total == 1 ? 'rule' : 'rules'} tested: " \
145
+ "#{Rainbow(success_count.to_s + ' successful').green.bold}, #{Rainbow(failed_rule_ids.size.to_s + ' failed').red.bold}"
138
146
 
139
- test_pass
147
+ failed_rule_ids.empty?
140
148
  end
141
149
 
142
150
  def rule_matches_example?(rule, trigger, example)
@@ -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
@@ -2,7 +2,7 @@ module Goodcheck
2
2
  class ConfigLoader
3
3
  include ArrayHelper
4
4
 
5
- class InvalidPattern < StandardError; end
5
+ class InvalidPattern < Error; end
6
6
 
7
7
  Schema = StrongJSON.new do
8
8
  def self.array_or(type)
@@ -22,7 +22,8 @@ module Goodcheck
22
22
  let :deprecated_token_pattern, object(token: string, case_insensitive: boolean?)
23
23
 
24
24
  let :encoding, enum(*Encoding.name_list.map {|name| literal(name) })
25
- let :glob_obj, object(pattern: string, encoding: optional(encoding))
25
+ let :glob_obj, object(pattern: string, encoding: optional(encoding),
26
+ exclude: enum?(string, array(string)))
26
27
  let :one_glob, enum(glob_obj,
27
28
  string,
28
29
  detector: -> (value) {
@@ -102,7 +103,8 @@ module Goodcheck
102
103
  justification: optional(array_or(string)),
103
104
  glob: optional(glob),
104
105
  pass: optional(array_or(string)),
105
- fail: optional(array_or(string))
106
+ fail: optional(array_or(string)),
107
+ severity: optional(string)
106
108
  )
107
109
 
108
110
  let :negative_rule, object(
@@ -112,14 +114,16 @@ module Goodcheck
112
114
  justification: optional(array_or(string)),
113
115
  glob: optional(glob),
114
116
  pass: optional(array_or(string)),
115
- fail: optional(array_or(string))
117
+ fail: optional(array_or(string)),
118
+ severity: optional(string)
116
119
  )
117
120
 
118
121
  let :nopattern_rule, object(
119
122
  id: string,
120
123
  message: string,
121
124
  justification: optional(array_or(string)),
122
- glob: glob
125
+ glob: glob,
126
+ severity: optional(string)
123
127
  )
124
128
 
125
129
  let :positive_trigger, object(
@@ -162,7 +166,8 @@ module Goodcheck
162
166
  id: string,
163
167
  message: string,
164
168
  justification: optional(array_or(string)),
165
- trigger: array_or(trigger)
169
+ trigger: array_or(trigger),
170
+ severity: optional(string)
166
171
  )
167
172
 
168
173
  let :rule, enum(positive_rule,
@@ -186,14 +191,17 @@ module Goodcheck
186
191
 
187
192
  let :rules, array(rule)
188
193
 
189
- let :import_target, string
190
- let :imports, array(import_target)
191
- let :exclude, array_or(string)
194
+ let :severity, object(
195
+ allow: optional(array(string)),
196
+ required: boolean?
197
+ )
192
198
 
193
199
  let :config, object(
194
200
  rules: optional(rules),
195
- import: optional(imports),
196
- exclude: optional(exclude)
201
+ import: optional(array(string)),
202
+ exclude: optional(array_or(string)),
203
+ exclude_binary: boolean?,
204
+ severity: optional(severity)
197
205
  )
198
206
  end
199
207
 
@@ -213,39 +221,40 @@ module Goodcheck
213
221
 
214
222
  def load
215
223
  Goodcheck.logger.info "Loading configuration: #{path}"
216
- Goodcheck.logger.tagged "#{path}" do
217
- Schema.config.coerce(content)
224
+ Schema.config.coerce(content)
218
225
 
219
- rules = []
226
+ rules = []
220
227
 
221
- load_rules(rules, array(content[:rules]))
222
-
223
- Array(content[:import]).each do |import|
224
- load_import rules, import
225
- end
228
+ load_rules(rules, array(content[:rules]))
226
229
 
227
- exclude_paths = Array(content[:exclude])
228
-
229
- Config.new(rules: rules, exclude_paths: exclude_paths)
230
+ Array(content[:import]).each do |import|
231
+ load_import rules, import
230
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
+ )
231
240
  end
232
241
 
233
242
  def load_rules(rules, array)
234
243
  array.each do |hash|
235
244
  rules << load_rule(hash)
245
+ rescue RegexpError => exn
246
+ raise InvalidPattern, "Invalid pattern of the `#{hash.fetch(:id)}` rule in `#{path}`: #{exn.message}"
236
247
  end
237
248
  end
238
249
 
239
250
  def load_import(rules, import)
240
251
  Goodcheck.logger.info "Importing rules from #{import}"
241
252
 
242
- Goodcheck.logger.tagged import do
243
- import_loader.load(import) do |content|
244
- 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)
245
255
 
246
- Schema.rules.coerce json
247
- load_rules(rules, json)
248
- end
256
+ Schema.rules.coerce json
257
+ load_rules(rules, json)
249
258
  end
250
259
  end
251
260
 
@@ -256,8 +265,9 @@ module Goodcheck
256
265
  triggers = retrieve_triggers(hash)
257
266
  justifications = array(hash[:justification])
258
267
  message = hash[:message].chomp
268
+ severity = hash[:severity]
259
269
 
260
- Rule.new(id: id, message: message, justifications: justifications, triggers: triggers)
270
+ Rule.new(id: id, message: message, justifications: justifications, triggers: triggers, severity: severity)
261
271
  end
262
272
 
263
273
  def retrieve_triggers(hash)
@@ -344,9 +354,9 @@ module Goodcheck
344
354
  globs.map do |glob|
345
355
  case glob
346
356
  when String
347
- Glob.new(pattern: glob, encoding: nil)
357
+ Glob.new(pattern: glob, encoding: nil, exclude: nil)
348
358
  when Hash
349
- Glob.new(pattern: glob[:pattern], encoding: glob[:encoding])
359
+ Glob.new(pattern: glob[:pattern], encoding: glob[:encoding], exclude: glob[:exclude])
350
360
  end
351
361
  end
352
362
  end