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
@@ -0,0 +1,3 @@
1
+ module Goodcheck
2
+ class Error < StandardError; end
3
+ end
@@ -0,0 +1,8 @@
1
+ module Goodcheck
2
+ module ExitStatus
3
+ EXIT_SUCCESS = 0
4
+ EXIT_ERROR = 1
5
+ EXIT_MATCH = 2
6
+ EXIT_TEST_FAILED = 3
7
+ end
8
+ 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,36 @@
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
+
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
+
11
34
  attr_reader :cache_path
12
35
  attr_reader :expires_in
13
36
  attr_reader :force_download
@@ -21,23 +44,39 @@ module Goodcheck
21
44
  end
22
45
 
23
46
  def load(name, &block)
24
- uri = URI.parse(name)
47
+ uri = begin
48
+ URI.parse(name)
49
+ rescue URI::InvalidURIError
50
+ nil
51
+ end
25
52
 
26
- case uri.scheme
27
- when nil, "file"
28
- load_file uri, &block
53
+ case uri&.scheme
54
+ when nil
55
+ load_file name, &block
56
+ when "file"
57
+ load_file uri.path, &block
29
58
  when "http", "https"
30
59
  load_http uri, &block
31
60
  else
32
- raise UnexpectedSchemaError.new("Unexpected URI schema: #{uri.class.name}")
61
+ raise UnexpectedSchemaError.new(uri)
33
62
  end
34
63
  end
35
64
 
36
- def load_file(uri)
37
- path = (config_path.parent + uri.path)
38
-
39
- begin
40
- yield path.read
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
67
+ if files.empty?
68
+ raise FileNotFound.new(path)
69
+ else
70
+ files.each do |file|
71
+ Goodcheck.logger.info "Reading file: #{file}"
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
79
+ end
41
80
  end
42
81
  end
43
82
 
@@ -45,7 +84,7 @@ module Goodcheck
45
84
  Digest::SHA2.hexdigest(uri.to_s)
46
85
  end
47
86
 
48
- def load_http(uri)
87
+ def load_http(uri, &block)
49
88
  hash = cache_name(uri)
50
89
  path = cache_path + hash
51
90
 
@@ -71,13 +110,19 @@ module Goodcheck
71
110
  if download
72
111
  path.rmtree if path.exist?
73
112
  Goodcheck.logger.info "Downloading content..."
74
- content = http_get uri
75
- Goodcheck.logger.debug "Downloaded content: #{content[0, 1024].inspect}#{content.size > 1024 ? "..." : ""}"
76
- yield content
77
- write_cache uri, content
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
78
123
  else
79
124
  Goodcheck.logger.info "Reading content from cache..."
80
- yield path.read
125
+ block.call(path.read, path.to_path)
81
126
  end
82
127
  end
83
128
 
@@ -90,16 +135,41 @@ module Goodcheck
90
135
  def http_get(uri, limit = 10)
91
136
  raise ArgumentError, "Too many HTTP redirects" if limit == 0
92
137
 
93
- res = Net::HTTP.get_response URI(uri)
94
- case res
95
- when Net::HTTPSuccess
96
- res.body
97
- when Net::HTTPRedirection
98
- location = res['Location']
99
- http_get location, limit - 1
100
- else
101
- raise "Error: HTTP GET #{uri.inspect} #{res.inspect}"
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 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
102
160
  end
103
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
104
174
  end
105
175
  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
@@ -5,6 +5,8 @@ module Goodcheck
5
5
 
6
6
  def initialize(stdout:)
7
7
  @stdout = stdout
8
+ @file_count = 0
9
+ @issue_count = 0
8
10
  end
9
11
 
10
12
  def analysis
@@ -12,6 +14,7 @@ module Goodcheck
12
14
  end
13
15
 
14
16
  def file(path)
17
+ @file_count += 1
15
18
  yield
16
19
  end
17
20
 
@@ -20,21 +23,51 @@ module Goodcheck
20
23
  end
21
24
 
22
25
  def issue(issue)
26
+ @issue_count += 1
27
+
28
+ message = issue.rule.message.lines.first.chomp
29
+
23
30
  if issue.location
24
- line = issue.buffer.line(issue.location.start_line)
25
- end_column = if issue.location.start_line == issue.location.end_line
26
- issue.location.end_column
27
- else
28
- line.bytesize
29
- end
30
- colored_line = line.byteslice(0, issue.location.start_column) + Rainbow(line.byteslice(issue.location.start_column, end_column - issue.location.start_column)).red + line.byteslice(end_column, line.bytesize)
31
- stdout.puts "#{issue.path}:#{issue.location.start_line}:#{colored_line.chomp}:\t#{issue.rule.message.lines.first.chomp}"
31
+ start_line = issue.location.start_line
32
+ start_column = issue.location.start_column
33
+ start_column_index = start_column - 1
34
+ line = issue.buffer.line(start_line)
35
+ column_size = if issue.location.one_line?
36
+ issue.location.column_size
37
+ else
38
+ line.bytesize - start_column
39
+ end
40
+ rule = Rainbow("(#{issue.rule.id})").darkgray
41
+ severity = issue.rule.severity ? Rainbow("[#{issue.rule.severity}]").magenta : ""
42
+ stdout.puts "#{Rainbow(issue.path).cyan}:#{start_line}:#{start_column}: #{message} #{rule} #{severity}".strip
43
+ stdout.puts line.chomp
44
+ stdout.puts (" " * start_column_index) + Rainbow("^" + "~" * (column_size - 1)).yellow
32
45
  else
33
- line = issue.buffer.line(1)&.chomp
34
- line = line ? Rainbow(line).red : '-'
35
- stdout.puts "#{issue.path}:-:#{line}:\t#{issue.rule.message.lines.first.chomp}"
46
+ stdout.puts "#{Rainbow(issue.path).cyan}:-:-: #{message}"
36
47
  end
37
48
  end
49
+
50
+ def summary
51
+ files = case @file_count
52
+ when 0
53
+ "no files"
54
+ when 1
55
+ "1 file"
56
+ else
57
+ "#{@file_count} files"
58
+ end
59
+ issues = case @issue_count
60
+ when 0
61
+ Rainbow("no issues").green
62
+ when 1
63
+ Rainbow("1 issue").red
64
+ else
65
+ Rainbow("#{@issue_count} issues").red
66
+ end
67
+
68
+ stdout.puts ""
69
+ stdout.puts "#{files} inspected, #{issues} detected"
70
+ end
38
71
  end
39
72
  end
40
73
  end
@@ -4,12 +4,14 @@ module Goodcheck
4
4
  attr_reader :triggers
5
5
  attr_reader :message
6
6
  attr_reader :justifications
7
+ attr_reader :severity
7
8
 
8
- def initialize(id:, triggers:, message:, justifications:)
9
+ def initialize(id:, triggers:, message:, justifications:, severity: nil)
9
10
  @id = id
10
11
  @triggers = triggers
11
12
  @message = message
12
13
  @justifications = justifications
14
+ @severity = severity
13
15
  end
14
16
  end
15
17
  end
@@ -0,0 +1,40 @@
1
+ module Goodcheck
2
+ class Unarchiver
3
+ attr_reader :file_filter
4
+
5
+ def initialize(file_filter: ->(_filename) { true })
6
+ @file_filter = file_filter
7
+ end
8
+
9
+ def tar_gz?(filename)
10
+ name = filename.to_s.downcase
11
+ ext = ".tar.gz"
12
+ name.end_with?(ext) && name != ext
13
+ end
14
+
15
+ def tar_gz(content)
16
+ require "rubygems/package"
17
+
18
+ Gem::Package::TarReader.new(StringIO.new(gz(content))) do |tar_reader|
19
+ tar_reader.each do |file|
20
+ if file.file? && file_filter.call(file.full_name)
21
+ yield file.read, file.full_name
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def gz(content)
30
+ require "zlib"
31
+
32
+ io = Zlib::GzipReader.new(StringIO.new(content))
33
+ begin
34
+ io.read
35
+ ensure
36
+ io.close
37
+ end
38
+ end
39
+ end
40
+ end