goodcheck 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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