goodcheck 2.5.0 → 2.7.0

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -2
  3. data/README.md +4 -447
  4. data/lib/goodcheck.rb +5 -5
  5. data/lib/goodcheck/analyzer.rb +13 -9
  6. data/lib/goodcheck/buffer.rb +11 -16
  7. data/lib/goodcheck/cli.rb +80 -56
  8. data/lib/goodcheck/commands/check.rb +42 -26
  9. data/lib/goodcheck/commands/config_loading.rb +20 -2
  10. data/lib/goodcheck/commands/init.rb +4 -2
  11. data/lib/goodcheck/commands/pattern.rb +2 -1
  12. data/lib/goodcheck/commands/test.rb +14 -11
  13. data/lib/goodcheck/config_loader.rb +17 -20
  14. data/lib/goodcheck/error.rb +3 -0
  15. data/lib/goodcheck/exit_status.rb +6 -0
  16. data/lib/goodcheck/glob.rb +14 -3
  17. data/lib/goodcheck/import_loader.rb +42 -10
  18. data/lib/goodcheck/issue.rb +3 -3
  19. data/lib/goodcheck/location.rb +28 -0
  20. data/lib/goodcheck/logger.rb +4 -4
  21. data/lib/goodcheck/reporters/json.rb +4 -0
  22. data/lib/goodcheck/reporters/text.rb +42 -11
  23. data/lib/goodcheck/version.rb +1 -1
  24. metadata +29 -92
  25. data/.github/workflows/test.yml +0 -37
  26. data/.gitignore +0 -13
  27. data/.rubocop.yml +0 -5
  28. data/Dockerfile +0 -13
  29. data/Gemfile +0 -6
  30. data/Rakefile +0 -70
  31. data/benchmark/gc.c +0 -12221
  32. data/bin/console +0 -14
  33. data/bin/setup +0 -8
  34. data/cheatsheet.pdf +0 -0
  35. data/docusaurus/.dockerignore +0 -2
  36. data/docusaurus/.gitignore +0 -12
  37. data/docusaurus/Dockerfile +0 -10
  38. data/docusaurus/docker-compose.yml +0 -18
  39. data/docusaurus/docs/commands.md +0 -69
  40. data/docusaurus/docs/configuration.md +0 -300
  41. data/docusaurus/docs/development.md +0 -15
  42. data/docusaurus/docs/getstarted.md +0 -46
  43. data/docusaurus/docs/rules.md +0 -79
  44. data/docusaurus/website/README.md +0 -193
  45. data/docusaurus/website/core/Footer.js +0 -100
  46. data/docusaurus/website/package.json +0 -14
  47. data/docusaurus/website/pages/en/index.js +0 -207
  48. data/docusaurus/website/pages/en/versions.js +0 -118
  49. data/docusaurus/website/sidebars.json +0 -11
  50. data/docusaurus/website/siteConfig.js +0 -171
  51. data/docusaurus/website/static/css/code-block-buttons.css +0 -39
  52. data/docusaurus/website/static/css/custom.css +0 -245
  53. data/docusaurus/website/static/img/favicon.ico +0 -0
  54. data/docusaurus/website/static/js/code-block-buttons.js +0 -47
  55. data/docusaurus/website/versioned_docs/version-1.0.0/commands.md +0 -70
  56. data/docusaurus/website/versioned_docs/version-1.0.0/configuration.md +0 -296
  57. data/docusaurus/website/versioned_docs/version-1.0.0/development.md +0 -16
  58. data/docusaurus/website/versioned_docs/version-1.0.0/getstarted.md +0 -47
  59. data/docusaurus/website/versioned_docs/version-1.0.0/rules.md +0 -81
  60. data/docusaurus/website/versioned_docs/version-1.0.2/rules.md +0 -79
  61. data/docusaurus/website/versioned_docs/version-2.4.0/configuration.md +0 -301
  62. data/docusaurus/website/versioned_docs/version-2.4.3/rules.md +0 -80
  63. data/docusaurus/website/versioned_sidebars/version-1.0.0-sidebars.json +0 -11
  64. data/docusaurus/website/versioned_sidebars/version-1.0.2-sidebars.json +0 -11
  65. data/docusaurus/website/versioned_sidebars/version-2.4.0-sidebars.json +0 -11
  66. data/docusaurus/website/versions.json +0 -10
  67. data/docusaurus/website/yarn.lock +0 -6806
  68. data/goodcheck.gemspec +0 -36
  69. data/goodcheck.yml +0 -10
  70. data/logo/GoodCheck Horizontal.pdf +0 -899
  71. data/logo/GoodCheck Horizontal.png +0 -0
  72. data/logo/GoodCheck Horizontal.svg +0 -55
  73. data/logo/GoodCheck logo.png +0 -0
  74. data/logo/GoodCheck vertical.png +0 -0
  75. data/sample.yml +0 -57
@@ -1,11 +1,27 @@
1
1
  module Goodcheck
2
2
  module Commands
3
3
  module ConfigLoading
4
+ class ConfigFileNotFound < Error
5
+ attr_reader :path
6
+
7
+ def initialize(path:)
8
+ super(path.to_s)
9
+ @path = path
10
+ end
11
+ end
12
+
4
13
  attr_reader :config
5
14
 
6
15
  def load_config!(force_download:, cache_path:)
16
+ config_content =
17
+ begin
18
+ config_path.read
19
+ rescue Errno::ENOENT
20
+ raise ConfigFileNotFound.new(path: config_path)
21
+ end
22
+
7
23
  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)
24
+ content = JSON.parse(JSON.dump(YAML.safe_load(config_content, filename: config_path.to_s)), symbolize_names: true)
9
25
  loader = ConfigLoader.new(path: config_path, content: content, stderr: stderr, import_loader: import_loader)
10
26
  @config = loader.load
11
27
  end
@@ -13,7 +29,9 @@ module Goodcheck
13
29
  def handle_config_errors(stderr)
14
30
  begin
15
31
  yield
16
-
32
+ rescue ConfigFileNotFound => exn
33
+ stderr.puts "Configuration file not found: #{exn.path}"
34
+ 1
17
35
  rescue Psych::Exception => exn
18
36
  stderr.puts "Unexpected error happens while loading YAML file: #{exn.inspect}"
19
37
  exn.backtrace.each do |trace_loc|
@@ -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,13 +25,13 @@ 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_ERROR
32
+ validate_rules or return EXIT_ERROR
32
33
 
33
- 0
34
+ EXIT_SUCCESS
34
35
  end
35
36
  end
36
37
 
@@ -50,7 +51,8 @@ module Goodcheck
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(Rainbow(" Found #{count} #{duplication}.😞").red)
54
56
  duplicated_ids.each do |id|
55
57
  stdout.puts " #{id}"
56
58
  end
@@ -75,7 +77,7 @@ module Goodcheck
75
77
  if trigger.by_pattern?
76
78
  stdout.puts " Testing pattern..."
77
79
  else
78
- stdout.puts " Testing #{(index+1).ordinalize} trigger..."
80
+ stdout.puts " #{index + 1}. Testing trigger..."
79
81
  end
80
82
 
81
83
  pass_errors = trigger.passes.each.with_index.select do |pass, _|
@@ -91,7 +93,7 @@ module Goodcheck
91
93
  rule_ok = false
92
94
 
93
95
  pass_errors.each do |_, index|
94
- stdout.puts " #{(index+1).ordinalize} #{Rainbow('pass').green} example matched.😱"
96
+ stdout.puts " #{index + 1}. #{Rainbow('pass').green} example matched.😱"
95
97
  failed_rule_ids << rule.id
96
98
  end
97
99
  end
@@ -101,7 +103,7 @@ module Goodcheck
101
103
  rule_ok = false
102
104
 
103
105
  fail_errors.each do |_, index|
104
- stdout.puts " #{(index+1).ordinalize} #{Rainbow('fail').red} example didn't match.😱"
106
+ stdout.puts " #{index + 1}. #{Rainbow('fail').red} example didn't match.😱"
105
107
  failed_rule_ids << rule.id
106
108
  end
107
109
  end
@@ -131,10 +133,11 @@ module Goodcheck
131
133
  end
132
134
 
133
135
  rule_count = success_count + failure_count
136
+ rule = rule_count == 1 ? "1 rule" : "#{rule_count} rules"
137
+ success = Rainbow(success_count == 1 ? "1 success" : "#{success_count} successes").green
138
+ failure = Rainbow(failure_count == 1 ? "1 failure" : "#{failure_count} failures").red
134
139
  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(", ")
140
+ stdout.puts "Tested #{rule}, #{success}, #{failure}"
138
141
 
139
142
  test_pass
140
143
  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) {
@@ -213,21 +214,19 @@ module Goodcheck
213
214
 
214
215
  def load
215
216
  Goodcheck.logger.info "Loading configuration: #{path}"
216
- Goodcheck.logger.tagged "#{path}" do
217
- Schema.config.coerce(content)
217
+ Schema.config.coerce(content)
218
218
 
219
- rules = []
219
+ rules = []
220
220
 
221
- load_rules(rules, array(content[:rules]))
221
+ load_rules(rules, array(content[:rules]))
222
222
 
223
- Array(content[:import]).each do |import|
224
- load_import rules, import
225
- end
223
+ Array(content[:import]).each do |import|
224
+ load_import rules, import
225
+ end
226
226
 
227
- exclude_paths = Array(content[:exclude])
227
+ exclude_paths = Array(content[:exclude])
228
228
 
229
- Config.new(rules: rules, exclude_paths: exclude_paths)
230
- end
229
+ Config.new(rules: rules, exclude_paths: exclude_paths)
231
230
  end
232
231
 
233
232
  def load_rules(rules, array)
@@ -239,13 +238,11 @@ module Goodcheck
239
238
  def load_import(rules, import)
240
239
  Goodcheck.logger.info "Importing rules from #{import}"
241
240
 
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)
241
+ import_loader.load(import) do |content|
242
+ json = JSON.parse(JSON.dump(YAML.load(content, filename: import)), symbolize_names: true)
245
243
 
246
- Schema.rules.coerce json
247
- load_rules(rules, json)
248
- end
244
+ Schema.rules.coerce json
245
+ load_rules(rules, json)
249
246
  end
250
247
  end
251
248
 
@@ -344,9 +341,9 @@ module Goodcheck
344
341
  globs.map do |glob|
345
342
  case glob
346
343
  when String
347
- Glob.new(pattern: glob, encoding: nil)
344
+ Glob.new(pattern: glob, encoding: nil, exclude: nil)
348
345
  when Hash
349
- Glob.new(pattern: glob[:pattern], encoding: glob[:encoding])
346
+ Glob.new(pattern: glob[:pattern], encoding: glob[:encoding], exclude: glob[:exclude])
350
347
  end
351
348
  end
352
349
  end
@@ -0,0 +1,3 @@
1
+ module Goodcheck
2
+ class Error < StandardError; end
3
+ end
@@ -0,0 +1,6 @@
1
+ module Goodcheck
2
+ module ExitStatus
3
+ EXIT_SUCCESS = 0
4
+ EXIT_ERROR = 1
5
+ end
6
+ end
@@ -1,21 +1,32 @@
1
1
  module Goodcheck
2
2
  class Glob
3
+ FNM_FLAGS = File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH
4
+
3
5
  attr_reader :pattern
4
6
  attr_reader :encoding
7
+ attr_reader :exclude
5
8
 
6
- def initialize(pattern:, encoding:)
9
+ def initialize(pattern:, encoding:, exclude:)
7
10
  @pattern = pattern
8
11
  @encoding = encoding
12
+ @exclude = exclude
9
13
  end
10
14
 
11
15
  def test(path)
12
- path.fnmatch?(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH)
16
+ path.fnmatch?(pattern, FNM_FLAGS) && !excluded?(path)
13
17
  end
14
18
 
15
19
  def ==(other)
16
20
  other.is_a?(Glob) &&
17
21
  other.pattern == pattern &&
18
- other.encoding == encoding
22
+ other.encoding == encoding &&
23
+ other.exclude == exclude
24
+ end
25
+
26
+ private
27
+
28
+ def excluded?(path)
29
+ Array(exclude).any? { |exc| path.fnmatch?(exc, FNM_FLAGS) }
19
30
  end
20
31
  end
21
32
  end
@@ -1,13 +1,23 @@
1
1
  module Goodcheck
2
2
  class ImportLoader
3
- class UnexpectedSchemaError < StandardError
3
+ class UnexpectedSchemaError < Error
4
4
  attr_reader :uri
5
5
 
6
6
  def initialize(uri)
7
+ super("Unexpected URI schema: #{uri.scheme}")
7
8
  @uri = uri
8
9
  end
9
10
  end
10
11
 
12
+ class FileNotFound < Error
13
+ attr_reader :path
14
+
15
+ def initialize(path)
16
+ super("No such a file: #{path}")
17
+ @path = path
18
+ end
19
+ end
20
+
11
21
  attr_reader :cache_path
12
22
  attr_reader :expires_in
13
23
  attr_reader :force_download
@@ -24,20 +34,26 @@ module Goodcheck
24
34
  uri = URI.parse(name)
25
35
 
26
36
  case uri.scheme
27
- when nil, "file"
28
- load_file uri, &block
37
+ when nil
38
+ load_file name, &block
39
+ when "file"
40
+ load_file uri.path, &block
29
41
  when "http", "https"
30
42
  load_http uri, &block
31
43
  else
32
- raise UnexpectedSchemaError.new("Unexpected URI schema: #{uri.class.name}")
44
+ raise UnexpectedSchemaError.new(uri)
33
45
  end
34
46
  end
35
47
 
36
- def load_file(uri)
37
- path = (config_path.parent + uri.path)
38
-
39
- begin
40
- yield path.read
48
+ def load_file(path)
49
+ files = Dir.glob(File.join(config_path.parent.to_path, path), File::FNM_DOTMATCH | File::FNM_EXTGLOB).sort
50
+ if files.empty?
51
+ raise FileNotFound.new(path)
52
+ else
53
+ files.each do |file|
54
+ Goodcheck.logger.info "Reading file: #{file}"
55
+ yield File.read(file)
56
+ end
41
57
  end
42
58
  end
43
59
 
@@ -71,7 +87,7 @@ module Goodcheck
71
87
  if download
72
88
  path.rmtree if path.exist?
73
89
  Goodcheck.logger.info "Downloading content..."
74
- content = HTTPClient.new.get_content(uri)
90
+ content = http_get uri
75
91
  Goodcheck.logger.debug "Downloaded content: #{content[0, 1024].inspect}#{content.size > 1024 ? "..." : ""}"
76
92
  yield content
77
93
  write_cache uri, content
@@ -85,5 +101,21 @@ module Goodcheck
85
101
  path = cache_path + cache_name(uri)
86
102
  path.write(content)
87
103
  end
104
+
105
+ # @see https://ruby-doc.org/stdlib-2.7.0/libdoc/net/http/rdoc/Net/HTTP.html#class-Net::HTTP-label-Following+Redirection
106
+ def http_get(uri, limit = 10)
107
+ raise ArgumentError, "Too many HTTP redirects" if limit == 0
108
+
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}"
118
+ end
119
+ end
88
120
  end
89
121
  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