goodcheck 2.6.0 β†’ 3.0.2

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.
data/lib/goodcheck.rb CHANGED
@@ -4,9 +4,7 @@ require "set"
4
4
  require "strong_json"
5
5
  require "yaml"
6
6
  require "json"
7
- require "active_support/core_ext/hash/indifferent_access"
8
- require "active_support/core_ext/integer/inflections"
9
- require "active_support/tagged_logging"
7
+ require "logger"
10
8
  require "rainbow"
11
9
  require "digest/sha2"
12
10
  require "net/http"
@@ -35,3 +33,8 @@ require "goodcheck/commands/init"
35
33
  require "goodcheck/commands/test"
36
34
  require "goodcheck/import_loader"
37
35
  require "goodcheck/commands/pattern"
36
+ require "goodcheck/unarchiver"
37
+
38
+ module Goodcheck
39
+ DEFAULT_CONFIG_FILE = "goodcheck.yml".freeze
40
+ end
@@ -13,7 +13,7 @@ module Goodcheck
13
13
  def scan(&block)
14
14
  if block_given?
15
15
  if trigger.patterns.empty?
16
- yield Issue.new(buffer: buffer, range: nil, rule: rule, text: nil)
16
+ yield Issue.new(buffer: buffer, rule: rule)
17
17
  else
18
18
  var_pats, novar_pats = trigger.patterns.partition {|pat|
19
19
  pat.is_a?(Pattern::Token) && !pat.variables.empty?
@@ -44,9 +44,7 @@ module Goodcheck
44
44
  while true
45
45
  case
46
46
  when scanner.scan_until(regexp)
47
- text = scanner.matched
48
- range = (scanner.pos - text.bytesize) .. scanner.pos
49
- issues << Issue.new(buffer: buffer, range: range, rule: rule, text: text)
47
+ issues << new_issue_with_matched(scanner)
50
48
  else
51
49
  break
52
50
  end
@@ -55,7 +53,7 @@ module Goodcheck
55
53
  issues.each(&block)
56
54
  else
57
55
  unless regexp =~ buffer.content
58
- yield Issue.new(buffer: buffer, range: nil, rule: rule, text: nil)
56
+ yield Issue.new(buffer: buffer, rule: rule)
59
57
  end
60
58
  end
61
59
  end
@@ -68,9 +66,7 @@ module Goodcheck
68
66
  case
69
67
  when scanner.scan_until(pat.regexp)
70
68
  if pat.test_variables(scanner)
71
- text = scanner.matched
72
- range = (scanner.pos - text.bytesize) .. scanner.pos
73
- yield Issue.new(buffer: buffer, range: range, rule: rule, text: text)
69
+ yield new_issue_with_matched(scanner)
74
70
  end
75
71
  else
76
72
  break
@@ -84,11 +80,19 @@ module Goodcheck
84
80
  break
85
81
  end
86
82
  else
87
- yield Issue.new(buffer: buffer, range: nil, rule: rule, text: nil)
83
+ yield Issue.new(buffer: buffer, rule: rule)
88
84
  break
89
85
  end
90
86
  end
91
87
  end
92
88
  end
89
+
90
+ private
91
+
92
+ def new_issue_with_matched(scanner)
93
+ Issue.new(buffer: buffer, rule: rule,
94
+ text: scanner.matched,
95
+ text_begin_pos: scanner.pos - scanner.matched_size)
96
+ end
93
97
  end
94
98
  end
@@ -1,8 +1,5 @@
1
1
  module Goodcheck
2
2
  class Buffer
3
- attr_reader :path
4
- attr_reader :content
5
-
6
3
  DISABLE_LINE_PATTERNS = [
7
4
  /\/\/ goodcheck-disable-line$/, #JS, Java, C, ...
8
5
  /# goodcheck-disable-line$/, # Ruby, Python, PHP, ...
@@ -25,10 +22,8 @@ module Goodcheck
25
22
  /' goodcheck-disable-next-line$/, # VB
26
23
  ].freeze
27
24
 
28
- class << self
29
- attr_accessor :DISABLE_LINE_PATTERNS
30
- attr_accessor :DISABLE_NEXT_LINE_PATTERNS
31
- end
25
+ attr_reader :path
26
+ attr_reader :content
32
27
 
33
28
  def initialize(path:, content:)
34
29
  @path = path
@@ -43,7 +38,7 @@ module Goodcheck
43
38
  start_position = 0
44
39
 
45
40
  content.split(/\n/, -1).each do |line|
46
- range = start_position...(start_position + line.bytesize)
41
+ range = start_position..(start_position + line.bytesize)
47
42
  @line_ranges << range
48
43
  start_position = range.end + 1
49
44
  end
@@ -51,10 +46,10 @@ module Goodcheck
51
46
 
52
47
  @line_ranges
53
48
  end
54
-
49
+
55
50
  def line_disabled?(line_number)
56
51
  if line_number > 1
57
- return true if DISABLE_NEXT_LINE_PATTERNS.any? { |pattern| line(line_number - 1).match?(pattern) }
52
+ return true if DISABLE_NEXT_LINE_PATTERNS.any? { |pattern| line(line_number - 1).match?(pattern) }
58
53
  end
59
54
 
60
55
  if line_number <= lines.length
@@ -70,7 +65,9 @@ module Goodcheck
70
65
  end
71
66
 
72
67
  if line_index
73
- [line_index + 1, position - line_ranges[line_index].begin]
68
+ line_number = line_index + 1
69
+ column_number = position - line_ranges[line_index].begin + 1
70
+ [line_number, column_number]
74
71
  end
75
72
  end
76
73
 
@@ -79,16 +76,7 @@ module Goodcheck
79
76
  end
80
77
 
81
78
  def line(line_number)
82
- lines[line_number-1]
83
- end
84
-
85
- def position_for_location(line, column)
86
- if (range = line_ranges[line-1])
87
- pos = range.begin + column
88
- if pos <= range.end
89
- pos
90
- end
91
- end
79
+ lines[line_number - 1]
92
80
  end
93
81
  end
94
82
  end
data/lib/goodcheck/cli.rb CHANGED
@@ -21,8 +21,6 @@ module Goodcheck
21
21
  help: "Show help and quit"
22
22
  }.freeze
23
23
 
24
- DEFAULT_CONFIG_FILE = Pathname("goodcheck.yml").freeze
25
-
26
24
  def run(args)
27
25
  command = args.shift&.to_sym
28
26
 
@@ -58,12 +56,12 @@ module Goodcheck
58
56
  end
59
57
 
60
58
  def check(args)
61
- config_path = DEFAULT_CONFIG_FILE
59
+ config_path = Pathname(DEFAULT_CONFIG_FILE)
62
60
  targets = []
63
61
  rules = []
64
62
  formats = [:text, :json]
65
63
  format = :text
66
- loglevel = Logger::ERROR
64
+ loglevel = nil
67
65
  force_download = false
68
66
 
69
67
  OptionParser.new("Usage: goodcheck check [options] paths...") do |opts|
@@ -82,7 +80,7 @@ module Goodcheck
82
80
  end
83
81
  end.parse!(args)
84
82
 
85
- Goodcheck.logger.level = loglevel
83
+ Goodcheck.logger.level = loglevel if loglevel
86
84
 
87
85
  if args.empty?
88
86
  targets << Pathname(".")
@@ -110,7 +108,7 @@ module Goodcheck
110
108
  end
111
109
 
112
110
  def test(args)
113
- config_path = DEFAULT_CONFIG_FILE
111
+ config_path = Pathname(DEFAULT_CONFIG_FILE)
114
112
  loglevel = ::Logger::ERROR
115
113
  force_download = false
116
114
 
@@ -132,7 +130,7 @@ module Goodcheck
132
130
  end
133
131
 
134
132
  def init(args)
135
- config_path = DEFAULT_CONFIG_FILE
133
+ config_path = Pathname(DEFAULT_CONFIG_FILE)
136
134
  force = false
137
135
 
138
136
  OptionParser.new("Usage: goodcheck init [options]") do |opts|
@@ -163,7 +161,7 @@ module Goodcheck
163
161
  end
164
162
 
165
163
  def pattern(args)
166
- config_path = DEFAULT_CONFIG_FILE
164
+ config_path = Pathname(DEFAULT_CONFIG_FILE)
167
165
 
168
166
  OptionParser.new do |opts|
169
167
  opts.banner = "Usage: goodcheck pattern [options] ids..."
@@ -15,8 +15,6 @@ module Goodcheck
15
15
  include HomePath
16
16
  include ExitStatus
17
17
 
18
- EXIT_MATCH = 2
19
-
20
18
  def initialize(config_path:, rules:, targets:, reporter:, stderr:, home_path:, force_download:)
21
19
  @config_path = config_path
22
20
  @rules = rules
@@ -57,6 +55,8 @@ module Goodcheck
57
55
  end
58
56
  end
59
57
 
58
+ reporter.summary
59
+
60
60
  issue_reported ? EXIT_MATCH : EXIT_SUCCESS
61
61
  end
62
62
  end
@@ -71,31 +71,27 @@ module Goodcheck
71
71
  def each_check
72
72
  targets.each do |target|
73
73
  Goodcheck.logger.info "Checking target: #{target}"
74
- Goodcheck.logger.tagged target.to_s do
75
- each_file target, immediate: true do |path|
76
- Goodcheck.logger.debug "Checking file: #{path}"
77
- Goodcheck.logger.tagged path.to_s do
78
- reporter.file(path) do
79
- buffers = {}
80
-
81
- config.rules_for_path(path, rules_filter: rules) do |rule, glob, trigger|
82
- Goodcheck.logger.debug "Checking rule: #{rule.id}"
83
- begin
84
- encoding = glob&.encoding || Encoding.default_external.name
85
-
86
- if buffers[encoding]
87
- buffer = buffers[encoding]
88
- else
89
- content = path.read(encoding: encoding).encode(Encoding.default_internal || Encoding::UTF_8)
90
- buffer = Buffer.new(path: path, content: content)
91
- buffers[encoding] = buffer
92
- end
93
-
94
- yield buffer, rule, trigger
95
- rescue ArgumentError => exn
96
- stderr.puts "#{path}: #{exn.inspect}"
97
- end
74
+ each_file target, immediate: true do |path|
75
+ Goodcheck.logger.debug "Checking file: #{path}"
76
+ reporter.file(path) do
77
+ buffers = {}
78
+
79
+ config.rules_for_path(path, rules_filter: rules) do |rule, glob, trigger|
80
+ Goodcheck.logger.debug "Checking rule: #{rule.id}"
81
+ begin
82
+ encoding = glob&.encoding || Encoding.default_external.name
83
+
84
+ if buffers[encoding]
85
+ buffer = buffers[encoding]
86
+ else
87
+ content = path.read(encoding: encoding).encode(Encoding.default_internal || Encoding::UTF_8)
88
+ buffer = Buffer.new(path: path, content: content)
89
+ buffers[encoding] = buffer
98
90
  end
91
+
92
+ yield buffer, rule, trigger
93
+ rescue ArgumentError => exn
94
+ stderr.puts "#{path}: #{exn.inspect}"
99
95
  end
100
96
  end
101
97
  end
@@ -112,7 +108,7 @@ module Goodcheck
112
108
  when DEFAULT_EXCLUSIONS.include?(path.basename.to_s)
113
109
  # noop
114
110
  when immediate || !excluded?(path)
115
- path.children.each do |child|
111
+ path.children.sort.each do |child|
116
112
  each_file(child, &block)
117
113
  end
118
114
  end
@@ -131,7 +127,7 @@ module Goodcheck
131
127
  end
132
128
 
133
129
  def excluded?(path)
134
- config.exclude_paths.any? {|pattern| path.fnmatch?(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
130
+ config.exclude_path?(path)
135
131
  end
136
132
  end
137
133
  end
@@ -1,10 +1,13 @@
1
1
  module Goodcheck
2
2
  module Commands
3
3
  module ConfigLoading
4
+ include ExitStatus
5
+
4
6
  class ConfigFileNotFound < Error
5
7
  attr_reader :path
6
8
 
7
9
  def initialize(path:)
10
+ super(path.to_s)
8
11
  @path = path
9
12
  end
10
13
  end
@@ -20,7 +23,7 @@ module Goodcheck
20
23
  end
21
24
 
22
25
  import_loader = ImportLoader.new(cache_path: cache_path, force_download: force_download, config_path: config_path)
23
- content = JSON.parse(JSON.dump(YAML.load(config_content, filename: config_path.to_s)), symbolize_names: true)
26
+ content = JSON.parse(JSON.dump(YAML.safe_load(config_content, filename: config_path.to_s)), symbolize_names: true)
24
27
  loader = ConfigLoader.new(path: config_path, content: content, stderr: stderr, import_loader: import_loader)
25
28
  @config = loader.load
26
29
  end
@@ -30,20 +33,23 @@ module Goodcheck
30
33
  yield
31
34
  rescue ConfigFileNotFound => exn
32
35
  stderr.puts "Configuration file not found: #{exn.path}"
33
- 1
36
+ EXIT_ERROR
37
+ rescue ConfigLoader::InvalidPattern => exn
38
+ stderr.puts exn.message
39
+ EXIT_ERROR
34
40
  rescue Psych::Exception => exn
35
41
  stderr.puts "Unexpected error happens while loading YAML file: #{exn.inspect}"
36
42
  exn.backtrace.each do |trace_loc|
37
43
  stderr.puts " #{trace_loc}"
38
44
  end
39
- 1
45
+ EXIT_ERROR
40
46
  rescue StrongJSON::Type::TypeError, StrongJSON::Type::UnexpectedAttributeError => exn
41
47
  stderr.puts "Invalid config: #{exn.message}"
42
48
  stderr.puts StrongJSON::ErrorReporter.new(path: exn.path).to_s
43
- 1
49
+ EXIT_ERROR
44
50
  rescue Errno::ENOENT => exn
45
51
  stderr.puts "#{exn}"
46
- 1
52
+ EXIT_ERROR
47
53
  end
48
54
  end
49
55
  end
@@ -28,15 +28,15 @@ module Goodcheck
28
28
  return EXIT_SUCCESS
29
29
  end
30
30
 
31
- validate_rule_uniqueness or return EXIT_ERROR
32
- validate_rules or return EXIT_ERROR
31
+ validate_rule_uniqueness or return EXIT_TEST_FAILED
32
+ validate_rules or return EXIT_TEST_FAILED
33
33
 
34
34
  EXIT_SUCCESS
35
35
  end
36
36
  end
37
37
 
38
38
  def validate_rule_uniqueness
39
- stdout.puts "Validating rule id uniqueness..."
39
+ stdout.puts "Validating rule ID uniqueness..."
40
40
 
41
41
  duplicated_ids = []
42
42
 
@@ -47,36 +47,35 @@ module Goodcheck
47
47
  end
48
48
 
49
49
  if duplicated_ids.empty?
50
- stdout.puts " OK!πŸ‘"
50
+ stdout.puts Rainbow(" OK! πŸ‘").green
51
51
  true
52
52
  else
53
53
  count = duplicated_ids.size
54
- stdout.puts(Rainbow(" Found #{count} #{'duplication'.pluralize(count)}.😞").red)
54
+ duplication = count == 1 ? 'duplication' : 'duplications'
55
+ stdout.puts " Found #{Rainbow(count).bold} #{duplication}. 😱"
55
56
  duplicated_ids.each do |id|
56
- stdout.puts " #{id}"
57
+ stdout.puts " - #{Rainbow(id).background(:red)}"
57
58
  end
58
59
  false
59
60
  end
60
61
  end
61
62
 
62
63
  def validate_rules
63
- test_pass = true
64
64
  success_count = 0
65
- failure_count = 0
66
65
  failed_rule_ids = Set[]
67
66
 
68
67
  config.rules.each do |rule|
69
- if rule.triggers.any? {|trigger| !trigger.passes.empty? || !trigger.fails.empty?}
70
- stdout.puts "Testing rule #{Rainbow(rule.id).cyan}..."
68
+ stdout.puts "Testing rule #{Rainbow(rule.id).cyan}..."
71
69
 
72
- rule_ok = true
70
+ rule_ok = true
73
71
 
72
+ if rule.triggers.any? {|trigger| !trigger.passes.empty? || !trigger.fails.empty?}
74
73
  rule.triggers.each.with_index do |trigger, index|
75
74
  if !trigger.passes.empty? || !trigger.fails.empty?
76
75
  if trigger.by_pattern?
77
76
  stdout.puts " Testing pattern..."
78
77
  else
79
- stdout.puts " Testing #{(index+1).ordinalize} trigger..."
78
+ stdout.puts " #{index + 1}. Testing trigger..."
80
79
  end
81
80
 
82
81
  pass_errors = trigger.passes.each.with_index.select do |pass, _|
@@ -88,21 +87,19 @@ module Goodcheck
88
87
  end
89
88
 
90
89
  unless pass_errors.empty?
91
- test_pass = false
92
90
  rule_ok = false
93
91
 
94
92
  pass_errors.each do |_, index|
95
- stdout.puts " #{(index+1).ordinalize} #{Rainbow('pass').green} example matched.😱"
93
+ stdout.puts " #{index + 1}. #{Rainbow('pass').green} example matched. 😱"
96
94
  failed_rule_ids << rule.id
97
95
  end
98
96
  end
99
97
 
100
98
  unless fail_errors.empty?
101
- test_pass = false
102
99
  rule_ok = false
103
100
 
104
101
  fail_errors.each do |_, index|
105
- stdout.puts " #{(index+1).ordinalize} #{Rainbow('fail').red} example didn't match.😱"
102
+ stdout.puts " #{index + 1}. #{Rainbow('fail').red} example didn’t match. 😱"
106
103
  failed_rule_ids << rule.id
107
104
  end
108
105
  end
@@ -110,16 +107,27 @@ module Goodcheck
110
107
  end
111
108
 
112
109
  if rule.triggers.any?(&:skips_fail_examples?)
113
- stdout.puts " 🚨 The rule contains a `pattern` with `glob`, which is not supported by the test command."
110
+ stdout.puts " The rule contains a `pattern` with `glob`, which is not supported by the test command. 🚨"
114
111
  stdout.puts " Skips testing `fail` examples."
115
112
  end
113
+ end
116
114
 
117
- if rule_ok
118
- stdout.puts " OK!πŸŽ‰"
119
- success_count += 1
120
- else
121
- failure_count += 1
122
- end
115
+ if rule.severity && !config.severity_allowed?(rule.severity)
116
+ allowed_severities = config.allowed_severities.map { |s| %("#{s}") }.join(', ')
117
+ stdout.puts Rainbow(" \"#{rule.severity}\" severity isn’t allowed. Must be one of #{allowed_severities}. 😱").red
118
+ rule_ok = false
119
+ failed_rule_ids << rule.id
120
+ end
121
+
122
+ if !rule.severity && config.severity_required?
123
+ stdout.puts Rainbow(" Severity is required. 😱").red
124
+ rule_ok = false
125
+ failed_rule_ids << rule.id
126
+ end
127
+
128
+ if rule_ok
129
+ stdout.puts Rainbow(" OK! πŸ‘").green
130
+ success_count += 1
123
131
  end
124
132
  end
125
133
 
@@ -131,13 +139,12 @@ module Goodcheck
131
139
  end
132
140
  end
133
141
 
134
- rule_count = success_count + failure_count
142
+ total = success_count + failed_rule_ids.size
135
143
  stdout.puts ""
136
- stdout.puts ["Tested #{rule_count} #{'rule'.pluralize(rule_count)}",
137
- Rainbow("#{success_count} #{'success'.pluralize(success_count)}").green,
138
- Rainbow("#{failure_count} #{'failure'.pluralize(failure_count)}").red].join(", ")
144
+ stdout.puts "#{Rainbow(total).bold} #{total == 1 ? 'rule' : 'rules'} tested: " \
145
+ "#{Rainbow(success_count.to_s + ' successful').green.bold}, #{Rainbow(failed_rule_ids.size.to_s + ' failed').red.bold}"
139
146
 
140
- test_pass
147
+ failed_rule_ids.empty?
141
148
  end
142
149
 
143
150
  def rule_matches_example?(rule, trigger, example)