goodcheck 2.5.2 → 3.0.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.
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