goodcheck 1.3.1 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4541711a6d0096718ec3099d059ea57c3af16b2b
4
- data.tar.gz: 88619a7566f492738fd93213896edeaedc05a076
3
+ metadata.gz: 9f4ea78ef7a3b4901947a2f42fd9a66789bcba33
4
+ data.tar.gz: 9a0b7b9028ae1c17ee38b0fd1225fb5e7dbca6f5
5
5
  SHA512:
6
- metadata.gz: 3226d5daece6e067f7ae2e123c3e43f4b2a7a0838e60cd92fef38f08721b3a91351782ed0a9c5d9ad96d38ad592355a3567e57eed292d7e053ede00f0874c3d5
7
- data.tar.gz: eecdde8f34d52b0b4479df6c3a7b16228b2ac89b2cfd642f7f3e4820dc3ea1f3d0ec828779227144931748e744eb164ee5b5d37a7791084f603544a74806d5c2
6
+ metadata.gz: 24db9a20284d894904e8fc8e4a631b2472363c958a9ee4bd3d007b1abad9eec0c00815d3e92b55b51f108011ec8d599132333b0af39857904818274134abd836
7
+ data.tar.gz: bd81681a1bee7c60a689f4ff9c3f4dd8f9296716dd5c16fda2af51ec55b92f4fa8b9d652821514ea4905bc7192ba5d955911a7fccd974a12459c1a4ac1a7a5ea
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.4.0 (2018-10-11)
6
+
7
+ * Exit with `2` when it find matching text #27
8
+ * Import rules from another location #26
9
+
5
10
  ## 1.3.1 (2018-08-16)
6
11
 
7
12
  * Delete Gemfile.lock
data/README.md CHANGED
@@ -154,6 +154,26 @@ If you write a string as a `glob`, the string value can be the `pattern` of the
154
154
 
155
155
  If you omit `glob` attribute in a rule, the rule will be applied to all files given to `goodcheck`.
156
156
 
157
+ ## Importing rules
158
+
159
+ `goodcheck.yml` can have optional `import` attribute.
160
+
161
+ ```yaml
162
+ rules: []
163
+ import:
164
+ - /usr/share/goodcheck/rules.yml
165
+ - lib/goodcheck/rules.yml
166
+ - https://some.host/shared/rules.yml
167
+ ```
168
+
169
+ Value of `import` can be an array of:
170
+
171
+ - A string which represents an absolute file path,
172
+ - A string which represents an relative file path from config file, or
173
+ - A http/https URL which represents the location of rules
174
+
175
+ The rules file is a YAML file with array of rules.
176
+
157
177
  ## Commands
158
178
 
159
179
  ### `goodcheck init [options]`
@@ -180,6 +200,17 @@ Available options are:
180
200
  * `-c [CONFIG]`, `--config=[CONFIG]` to specify the configuration file.
181
201
  * `-R [rule]`, `--rule=[rule]` to specify the rules you want to check.
182
202
  * `--format=[text|json]` to specify output format.
203
+ * `-v`, `--verbose` to be verbose.
204
+ * `--debug` to print all debug messages.
205
+ * `--force` to ignore downloaded caches
206
+
207
+ `goodcheck check` exits with:
208
+
209
+ * `0` when it does not find any matching text fragment
210
+ * `2` when it finds some matching text
211
+ * `1` when it finds some error
212
+
213
+ You can check its exit status to identify if the tool find some pattern or not.
183
214
 
184
215
  ### `goodcheck test [options]`
185
216
 
@@ -195,6 +226,16 @@ Use `test` command when you add new rule to be sure you are writing rules correc
195
226
  Available options is:
196
227
 
197
228
  * `-c [CONFIG]`, `--config=[CONFIG]` to specify the configuration file.
229
+ * `-v`, `--verbose` to be verbose.
230
+ * `--debug` to print all debug messages.
231
+ * `--force` to ignore downloaded caches
232
+
233
+ ## Downloaded rules
234
+
235
+ Downloaded rules are cached in `cache` directory in *goodcheck home directory*.
236
+ The *goodcheck home directory* is `~/.goodcheck`, but you can customize the location with `GOODCHECK_HOME` environment variable.
237
+
238
+ The cache expires in 3 minutes.
198
239
 
199
240
  ## Docker image
200
241
 
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.add_runtime_dependency "activesupport", "~> 5.0"
30
30
  spec.add_runtime_dependency "strong_json", "~> 0.5.0"
31
31
  spec.add_runtime_dependency "rainbow", "~> 3.0.0"
32
+ spec.add_runtime_dependency "httpclient", "~> 2.8.3"
32
33
  end
@@ -5,9 +5,14 @@ require "yaml"
5
5
  require "json"
6
6
  require "active_support/core_ext/hash/indifferent_access"
7
7
  require "active_support/core_ext/integer/inflections"
8
+ require "active_support/tagged_logging"
8
9
  require "rainbow"
10
+ require "digest/sha2"
11
+ require "httpclient"
9
12
 
10
13
  require "goodcheck/version"
14
+ require "goodcheck/logger"
15
+ require "goodcheck/home_path"
11
16
 
12
17
  require "goodcheck/glob"
13
18
  require "goodcheck/buffer"
@@ -25,3 +30,4 @@ require "goodcheck/commands/config_loading"
25
30
  require "goodcheck/commands/check"
26
31
  require "goodcheck/commands/init"
27
32
  require "goodcheck/commands/test"
33
+ require "goodcheck/import_loader"
@@ -1,5 +1,7 @@
1
1
  require "optparse"
2
2
 
3
+ Version = Goodcheck::VERSION
4
+
3
5
  module Goodcheck
4
6
  class CLI
5
7
  attr_reader :stdout
@@ -35,11 +37,21 @@ module Goodcheck
35
37
  1
36
38
  end
37
39
 
40
+ def home_path
41
+ if (path = ENV["GOODCHECK_HOME"])
42
+ Pathname(path)
43
+ else
44
+ Pathname(Dir.home) + ".goodcheck"
45
+ end
46
+ end
47
+
38
48
  def check(args)
39
49
  config_path = Pathname("goodcheck.yml")
40
50
  targets = []
41
51
  rules = []
42
52
  format = nil
53
+ loglevel = Logger::ERROR
54
+ force_download = false
43
55
 
44
56
  OptionParser.new("Usage: goodcheck check [options] dirs...") do |opts|
45
57
  opts.on("-c CONFIG", "--config=CONFIG") do |config|
@@ -51,8 +63,19 @@ module Goodcheck
51
63
  opts.on("--format=<text|json> [default: 'text']") do |f|
52
64
  format = f
53
65
  end
66
+ opts.on("-v", "--verbose") do
67
+ loglevel = Logger::INFO
68
+ end
69
+ opts.on("-d", "--debug") do
70
+ loglevel = Logger::DEBUG
71
+ end
72
+ opts.on("--force") do
73
+ force_download = true
74
+ end
54
75
  end.parse!(args)
55
76
 
77
+ Goodcheck.logger.level = loglevel
78
+
56
79
  if args.empty?
57
80
  targets << Pathname(".")
58
81
  else
@@ -69,19 +92,43 @@ module Goodcheck
69
92
  return 1
70
93
  end
71
94
 
72
- Commands::Check.new(reporter: reporter, config_path: config_path, rules: rules, targets: targets, stderr: stderr).run
95
+ Goodcheck.logger.info "Configuration = #{config_path}"
96
+ Goodcheck.logger.info "Rules = [#{rules.join(", ")}]"
97
+ Goodcheck.logger.info "Format = #{format}"
98
+ Goodcheck.logger.info "Targets = [#{targets.join(", ")}]"
99
+ Goodcheck.logger.info "Force download = #{force_download}"
100
+ Goodcheck.logger.info "Home path = #{home_path}"
101
+
102
+ Commands::Check.new(reporter: reporter, config_path: config_path, rules: rules, targets: targets, stderr: stderr, force_download: force_download, home_path: home_path).run
73
103
  end
74
104
 
75
105
  def test(args)
76
106
  config_path = Pathname("goodcheck.yml")
107
+ loglevel = Logger::ERROR
108
+ force_download = false
77
109
 
78
110
  OptionParser.new("Usage: goodcheck test [options]") do |opts|
79
111
  opts.on("-c CONFIG", "--config=CONFIG") do |config|
80
112
  config_path = Pathname(config)
81
113
  end
114
+ opts.on("-v", "--verbose") do
115
+ loglevel = Logger::INFO
116
+ end
117
+ opts.on("-d", "--debug") do
118
+ loglevel = Logger::DEBUG
119
+ end
120
+ opts.on("--force") do
121
+ force_download = true
122
+ end
82
123
  end.parse!(args)
83
124
 
84
- Commands::Test.new(stdout: stdout, stderr: stderr, config_path: config_path).run
125
+ Goodcheck.logger.level = loglevel
126
+
127
+ Goodcheck.logger.info "Configuration = #{config_path}"
128
+ Goodcheck.logger.info "Force download = #{force_download}"
129
+ Goodcheck.logger.info "Home path = #{home_path}"
130
+
131
+ Commands::Test.new(stdout: stdout, stderr: stderr, config_path: config_path, force_download: force_download, home_path: home_path).run
85
132
  end
86
133
 
87
134
  def init(args)
@@ -6,30 +6,39 @@ module Goodcheck
6
6
  attr_reader :targets
7
7
  attr_reader :reporter
8
8
  attr_reader :stderr
9
+ attr_reader :force_download
10
+ attr_reader :home_path
9
11
 
10
12
  include ConfigLoading
13
+ include HomePath
11
14
 
12
- def initialize(config_path:, rules:, targets:, reporter:, stderr:)
15
+ def initialize(config_path:, rules:, targets:, reporter:, stderr:, home_path:, force_download:)
13
16
  @config_path = config_path
14
17
  @rules = rules
15
18
  @targets = targets
16
19
  @reporter = reporter
17
20
  @stderr = stderr
21
+ @force_download = force_download
22
+ @home_path = home_path
18
23
  end
19
24
 
20
25
  def run
26
+ issue_reported = false
27
+
21
28
  reporter.analysis do
22
- load_config!
29
+ load_config!(force_download: force_download, cache_path: cache_dir_path)
23
30
  each_check do |buffer, rule|
24
31
  reporter.rule(rule) do
25
32
  analyzer = Analyzer.new(rule: rule, buffer: buffer)
26
33
  analyzer.scan do |issue|
34
+ issue_reported = true
27
35
  reporter.issue(issue)
28
36
  end
29
37
  end
30
38
  end
31
39
  end
32
- 0
40
+
41
+ issue_reported ? 2 : 0
33
42
  rescue Psych::Exception => exn
34
43
  stderr.puts "Unexpected error happens while loading YAML file: #{exn.inspect}"
35
44
  exn.backtrace.each do |trace_loc|
@@ -46,25 +55,32 @@ module Goodcheck
46
55
 
47
56
  def each_check
48
57
  targets.each do |target|
49
- each_file target, immediate: true do |path|
50
- reporter.file(path) do
51
- buffers = {}
58
+ Goodcheck.logger.info "Checking target: #{target}"
59
+ Goodcheck.logger.tagged target.to_s do
60
+ each_file target, immediate: true do |path|
61
+ Goodcheck.logger.debug "Checking file: #{path}"
62
+ Goodcheck.logger.tagged path.to_s do
63
+ reporter.file(path) do
64
+ buffers = {}
52
65
 
53
- config.rules_for_path(path, rules_filter: rules) do |rule, glob|
54
- begin
55
- encoding = glob&.encoding || Encoding.default_external.name
66
+ config.rules_for_path(path, rules_filter: rules) do |rule, glob|
67
+ Goodcheck.logger.debug "Checking rule: #{rule.id}"
68
+ begin
69
+ encoding = glob&.encoding || Encoding.default_external.name
56
70
 
57
- if buffers[encoding]
58
- buffer = buffers[encoding]
59
- else
60
- content = path.read(encoding: encoding).encode(Encoding.default_internal || Encoding::UTF_8)
61
- buffer = Buffer.new(path: path, content: content)
62
- buffers[encoding] = buffer
63
- end
71
+ if buffers[encoding]
72
+ buffer = buffers[encoding]
73
+ else
74
+ content = path.read(encoding: encoding).encode(Encoding.default_internal || Encoding::UTF_8)
75
+ buffer = Buffer.new(path: path, content: content)
76
+ buffers[encoding] = buffer
77
+ end
64
78
 
65
- yield buffer, rule
66
- rescue ArgumentError => exn
67
- stderr.puts "#{path}: #{exn.inspect}"
79
+ yield buffer, rule
80
+ rescue ArgumentError => exn
81
+ stderr.puts "#{path}: #{exn.inspect}"
82
+ end
83
+ end
68
84
  end
69
85
  end
70
86
  end
@@ -3,9 +3,10 @@ module Goodcheck
3
3
  module ConfigLoading
4
4
  attr_reader :config
5
5
 
6
- def load_config!
6
+ def load_config!(force_download:, cache_path:)
7
+ import_loader = ImportLoader.new(cache_path: cache_path, force_download: force_download, config_path: config_path)
7
8
  content = JSON.parse(JSON.dump(YAML.load(config_path.read, config_path.to_s)), symbolize_names: true)
8
- loader = ConfigLoader.new(path: config_path, content: content, stderr: stderr)
9
+ loader = ConfigLoader.new(path: config_path, content: content, stderr: stderr, import_loader: import_loader)
9
10
  @config = loader.load
10
11
  end
11
12
  end
@@ -2,19 +2,24 @@ module Goodcheck
2
2
  module Commands
3
3
  class Test
4
4
  include ConfigLoading
5
+ include HomePath
5
6
 
6
7
  attr_reader :stdout
7
8
  attr_reader :stderr
8
9
  attr_reader :config_path
10
+ attr_reader :home_path
11
+ attr_reader :force_download
9
12
 
10
- def initialize(stdout:, stderr:, config_path:)
13
+ def initialize(stdout:, stderr:, config_path:, force_download:, home_path:)
11
14
  @stdout = stdout
12
15
  @stderr = stderr
13
16
  @config_path = config_path
17
+ @force_download = force_download
18
+ @home_path = home_path
14
19
  end
15
20
 
16
21
  def run
17
- load_config!
22
+ load_config!(cache_path: cache_dir_path, force_download: force_download)
18
23
 
19
24
  validate_rule_uniqueness or return 1
20
25
  validate_rules or return 1
@@ -32,28 +32,65 @@ module Goodcheck
32
32
 
33
33
  let :rules, array(rule)
34
34
 
35
- let :config, object(rules: rules)
35
+ let :import_target, string
36
+ let :imports, array(import_target)
37
+
38
+ let :config, object(rules: rules, import: optional(imports))
36
39
  end
37
40
 
38
41
  attr_reader :path
39
42
  attr_reader :content
40
43
  attr_reader :stderr
41
44
  attr_reader :printed_warnings
45
+ attr_reader :import_loader
42
46
 
43
- def initialize(path:, content:, stderr:)
47
+ def initialize(path:, content:, stderr:, import_loader:)
44
48
  @path = path
45
49
  @content = content
46
50
  @stderr = stderr
47
51
  @printed_warnings = Set.new
52
+ @import_loader = import_loader
48
53
  end
49
54
 
50
55
  def load
51
- Schema.config.coerce(content)
52
- rules = content[:rules].map {|hash| load_rule(hash) }
53
- Config.new(rules: rules)
56
+ Goodcheck.logger.info "Loading configuration: #{path}"
57
+ Goodcheck.logger.tagged "#{path}" do
58
+ Schema.config.coerce(content)
59
+
60
+ rules = []
61
+
62
+ load_rules(rules, content[:rules])
63
+
64
+ Array(content[:import]).each do |import|
65
+ load_import rules, import
66
+ end
67
+
68
+ Config.new(rules: rules)
69
+ end
70
+ end
71
+
72
+ def load_rules(rules, array)
73
+ array.each do |hash|
74
+ rules << load_rule(hash)
75
+ end
76
+ end
77
+
78
+ def load_import(rules, import)
79
+ Goodcheck.logger.info "Importing rules from #{import}"
80
+
81
+ Goodcheck.logger.tagged import do
82
+ import_loader.load(import) do |content|
83
+ json = JSON.parse(JSON.dump(YAML.load(content, import)), symbolize_names: true)
84
+
85
+ Schema.rules.coerce json
86
+ load_rules(rules, json)
87
+ end
88
+ end
54
89
  end
55
90
 
56
91
  def load_rule(hash)
92
+ Goodcheck.logger.debug "Loading rule: #{hash[:id]}"
93
+
57
94
  id = hash[:id]
58
95
  patterns = retrieve_patterns(hash)
59
96
  justifications = array(hash[:justification])
@@ -0,0 +1,9 @@
1
+ module Goodcheck
2
+ module HomePath
3
+ def cache_dir_path
4
+ @cache_dir_path ||= (home_path + "cache").tap do |path|
5
+ path.mkpath unless path.directory?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,89 @@
1
+ module Goodcheck
2
+ class ImportLoader
3
+ class UnexpectedSchemaError < StandardError
4
+ attr_reader :uri
5
+
6
+ def initialize(uri)
7
+ @uri = uri
8
+ end
9
+ end
10
+
11
+ attr_reader :cache_path
12
+ attr_reader :expires_in
13
+ attr_reader :force_download
14
+ attr_reader :config_path
15
+
16
+ def initialize(cache_path:, expires_in: 3 * 60, force_download:, config_path:)
17
+ @cache_path = cache_path
18
+ @expires_in = expires_in
19
+ @force_download = force_download
20
+ @config_path = config_path
21
+ end
22
+
23
+ def load(name, &block)
24
+ uri = URI.parse(name)
25
+
26
+ case uri.scheme
27
+ when nil, "file"
28
+ load_file uri, &block
29
+ when "http", "https"
30
+ load_http uri, &block
31
+ else
32
+ raise UnexpectedSchemaError.new("Unexpected URI schema: #{uri.class.name}")
33
+ end
34
+ end
35
+
36
+ def load_file(uri)
37
+ path = (config_path.parent + uri.path)
38
+
39
+ begin
40
+ yield path.read
41
+ end
42
+ end
43
+
44
+ def cache_name(uri)
45
+ Digest::SHA2.hexdigest(uri.to_s)
46
+ end
47
+
48
+ def load_http(uri)
49
+ hash = cache_name(uri)
50
+ path = cache_path + hash
51
+
52
+ Goodcheck.logger.info "Calculated cache name: #{hash}"
53
+
54
+ download = false
55
+
56
+ if force_download
57
+ Goodcheck.logger.debug "Downloading: force flag"
58
+ download = true
59
+ end
60
+
61
+ if !download && !path.file?
62
+ Goodcheck.logger.debug "Downloading: no cache found"
63
+ download = true
64
+ end
65
+
66
+ if !download && path.mtime + expires_in < Time.now
67
+ Goodcheck.logger.debug "Downloading: cache expired"
68
+ download = true
69
+ end
70
+
71
+ if download
72
+ path.rmtree if path.exist?
73
+ Goodcheck.logger.info "Downloading content..."
74
+ content = HTTPClient.new.get_content(uri)
75
+ Goodcheck.logger.debug "Downloaded content: #{content[0, 1024].inspect}#{content.size > 1024 ? "..." : ""}"
76
+ yield content
77
+ write_cache uri, content
78
+ else
79
+ Goodcheck.logger.info "Reading content from cache..."
80
+ yield path.read
81
+ end
82
+ end
83
+
84
+ def write_cache(uri, content)
85
+ path = cache_path + cache_name(uri)
86
+ path.write(content)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,8 @@
1
+ module Goodcheck
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
7
+ end
8
+ end
@@ -12,7 +12,6 @@ module Goodcheck
12
12
  end
13
13
 
14
14
  def analysis
15
- stderr.puts "Starting analysis..."
16
15
  yield
17
16
 
18
17
  json = issues.map do |issue|
@@ -34,12 +33,10 @@ module Goodcheck
34
33
  end
35
34
 
36
35
  def file(path)
37
- stderr.puts "Checking #{path}..."
38
36
  yield
39
37
  end
40
38
 
41
39
  def rule(rule)
42
- stderr.puts " Checking #{rule.id}..."
43
40
  yield
44
41
  end
45
42
 
@@ -1,3 +1,3 @@
1
1
  module Goodcheck
2
- VERSION = "1.3.1"
2
+ VERSION = "1.4.0"
3
3
  end
data/sample.yml CHANGED
@@ -1,4 +1,15 @@
1
1
  rules:
2
+ - id: sample.typo
3
+ pattern:
4
+ - Github
5
+ - FaceBook
6
+ message: |
7
+ Write GitHub and Facebook
8
+
9
+ Their names are GitHub and Facebook.
10
+ Maybe, you are misspelling.
11
+ pass: GitHub
12
+ fail: Github
2
13
  - id: sample.debug_print
3
14
  pattern:
4
15
  - token: pp
@@ -10,19 +21,6 @@ rules:
10
21
  - render "app/views/welcome.html.erb"
11
22
  fail:
12
23
  - pp("Hello World")
13
- - id: sample.index_zero
14
- pattern:
15
- - token: "[0]"
16
- glob: "**/*.rb"
17
- message: |
18
- You can use #first instead of [0]
19
- pass: array.first
20
- fail: array[0]
21
- - id: sample.closed_range
22
- pattern:
23
- - token: ...
24
- glob: "**/*.rb"
25
- message: |
26
- Generally .. is better than ...
27
- fail: 1...3
28
- pass: 1..4
24
+
25
+ import:
26
+ - https://gist.githubusercontent.com/soutaro/6362c89acd7d6771ae6ebfc615be402d/raw/7f04b973c2c8df70783cd7deb955ab95d1375b2d/sample.yml
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: goodcheck
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Soutaro Matsumoto
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-08-16 00:00:00.000000000 Z
11
+ date: 2018-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: 3.0.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: httpclient
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.8.3
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.8.3
97
111
  description: Regexp based customizable linter
98
112
  email:
99
113
  - matsumoto@soutaro.com
@@ -126,8 +140,11 @@ files:
126
140
  - lib/goodcheck/config.rb
127
141
  - lib/goodcheck/config_loader.rb
128
142
  - lib/goodcheck/glob.rb
143
+ - lib/goodcheck/home_path.rb
144
+ - lib/goodcheck/import_loader.rb
129
145
  - lib/goodcheck/issue.rb
130
146
  - lib/goodcheck/location.rb
147
+ - lib/goodcheck/logger.rb
131
148
  - lib/goodcheck/pattern.rb
132
149
  - lib/goodcheck/reporters/json.rb
133
150
  - lib/goodcheck/reporters/text.rb