goodcheck 2.6.0 β†’ 3.0.2

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