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
@@ -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