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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/LICENSE +1 -1
- data/README.md +8 -445
- data/lib/goodcheck.rb +6 -3
- data/lib/goodcheck/analyzer.rb +13 -9
- data/lib/goodcheck/buffer.rb +9 -21
- data/lib/goodcheck/cli.rb +6 -8
- data/lib/goodcheck/commands/check.rb +24 -28
- data/lib/goodcheck/commands/config_loading.rb +11 -5
- data/lib/goodcheck/commands/test.rb +35 -28
- data/lib/goodcheck/config.rb +68 -1
- data/lib/goodcheck/config_loader.rb +36 -27
- data/lib/goodcheck/exit_status.rb +2 -0
- data/lib/goodcheck/import_loader.rb +74 -20
- data/lib/goodcheck/issue.rb +3 -3
- data/lib/goodcheck/location.rb +28 -0
- data/lib/goodcheck/logger.rb +4 -4
- data/lib/goodcheck/reporters/json.rb +6 -1
- data/lib/goodcheck/reporters/text.rb +44 -11
- data/lib/goodcheck/rule.rb +3 -1
- data/lib/goodcheck/unarchiver.rb +40 -0
- data/lib/goodcheck/version.rb +1 -1
- metadata +42 -30
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 "
|
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
|
data/lib/goodcheck/analyzer.rb
CHANGED
@@ -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,
|
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
|
-
|
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,
|
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
|
-
|
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,
|
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
|
data/lib/goodcheck/buffer.rb
CHANGED
@@ -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
|
-
|
29
|
-
|
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
|
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
|
-
|
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 =
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
49
|
+
EXIT_ERROR
|
44
50
|
rescue Errno::ENOENT => exn
|
45
51
|
stderr.puts "#{exn}"
|
46
|
-
|
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
|
32
|
-
validate_rules or return
|
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
|
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
|
-
|
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
|
-
|
70
|
-
stdout.puts "Testing rule #{Rainbow(rule.id).cyan}..."
|
68
|
+
stdout.puts "Testing rule #{Rainbow(rule.id).cyan}..."
|
71
69
|
|
72
|
-
|
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 "
|
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 " #{
|
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 " #{
|
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 "
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
142
|
+
total = success_count + failed_rule_ids.size
|
135
143
|
stdout.puts ""
|
136
|
-
stdout.puts
|
137
|
-
|
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
|
-
|
147
|
+
failed_rule_ids.empty?
|
141
148
|
end
|
142
149
|
|
143
150
|
def rule_matches_example?(rule, trigger, example)
|