goodcheck 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +661 -0
- data/README.md +200 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/goodcheck +7 -0
- data/goodcheck.gemspec +30 -0
- data/lib/goodcheck.rb +28 -0
- data/lib/goodcheck/analyzer.rb +46 -0
- data/lib/goodcheck/array_helper.rb +12 -0
- data/lib/goodcheck/buffer.rb +50 -0
- data/lib/goodcheck/cli.rb +112 -0
- data/lib/goodcheck/commands/check.rb +89 -0
- data/lib/goodcheck/commands/config_loading.rb +13 -0
- data/lib/goodcheck/commands/init.rb +48 -0
- data/lib/goodcheck/commands/test.rb +95 -0
- data/lib/goodcheck/config.rb +28 -0
- data/lib/goodcheck/config_loader.rb +90 -0
- data/lib/goodcheck/glob.rb +11 -0
- data/lib/goodcheck/issue.rb +29 -0
- data/lib/goodcheck/location.rb +23 -0
- data/lib/goodcheck/matcher.rb +21 -0
- data/lib/goodcheck/pattern.rb +57 -0
- data/lib/goodcheck/reporters/json.rb +50 -0
- data/lib/goodcheck/reporters/text.rb +34 -0
- data/lib/goodcheck/rule.rb +21 -0
- data/lib/goodcheck/version.rb +3 -0
- data/sample.yml +28 -0
- metadata +161 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
module Commands
|
3
|
+
class Check
|
4
|
+
attr_reader :config_path
|
5
|
+
attr_reader :rules
|
6
|
+
attr_reader :targets
|
7
|
+
attr_reader :reporter
|
8
|
+
attr_reader :stderr
|
9
|
+
|
10
|
+
include ConfigLoading
|
11
|
+
|
12
|
+
def initialize(config_path:, rules:, targets:, reporter:, stderr:)
|
13
|
+
@config_path = config_path
|
14
|
+
@rules = rules
|
15
|
+
@targets = targets
|
16
|
+
@reporter = reporter
|
17
|
+
@stderr = stderr
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
reporter.analysis do
|
22
|
+
load_config!
|
23
|
+
each_check do |buffer, rule|
|
24
|
+
reporter.rule(rule) do
|
25
|
+
analyzer = Analyzer.new(rule: rule, buffer: buffer)
|
26
|
+
analyzer.scan do |issue|
|
27
|
+
reporter.issue(issue)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
0
|
33
|
+
rescue Psych::Exception => exn
|
34
|
+
stderr.puts "Unexpected error happens while loading YAML file: #{exn.inspect}"
|
35
|
+
exn.backtrace.each do |trace_loc|
|
36
|
+
stderr.puts " #{trace_loc}"
|
37
|
+
end
|
38
|
+
1
|
39
|
+
rescue StrongJSON::Type::Error => exn
|
40
|
+
stderr.puts "Invalid config at #{exn.path.map {|x| "[#{x}]" }.join}"
|
41
|
+
1
|
42
|
+
rescue Errno::ENOENT => exn
|
43
|
+
stderr.puts "#{exn}"
|
44
|
+
1
|
45
|
+
end
|
46
|
+
|
47
|
+
def each_check
|
48
|
+
targets.each do |target|
|
49
|
+
each_file target do |path|
|
50
|
+
reporter.file(path) do
|
51
|
+
buffers = {}
|
52
|
+
|
53
|
+
config.rules_for_path(path, rules_filter: rules) do |rule, glob|
|
54
|
+
begin
|
55
|
+
encoding = glob&.encoding || Encoding.default_external.name
|
56
|
+
|
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
|
64
|
+
|
65
|
+
yield buffer, rule
|
66
|
+
rescue ArgumentError => exn
|
67
|
+
stderr.puts "#{path}: #{exn.inspect}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def each_file(path, &block)
|
76
|
+
realpath = path.realpath
|
77
|
+
|
78
|
+
case
|
79
|
+
when realpath.directory?
|
80
|
+
path.children.each do |child|
|
81
|
+
each_file(child, &block)
|
82
|
+
end
|
83
|
+
when realpath.file?
|
84
|
+
yield path
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
module Commands
|
3
|
+
module ConfigLoading
|
4
|
+
attr_reader :config
|
5
|
+
|
6
|
+
def load_config!
|
7
|
+
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)
|
9
|
+
@config = loader.load
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
module Commands
|
3
|
+
class Init
|
4
|
+
CONFIG = <<-EOC
|
5
|
+
rules:
|
6
|
+
- id: com.example.1
|
7
|
+
pattern: Github
|
8
|
+
message: Do you want to write GitHub?
|
9
|
+
glob:
|
10
|
+
- "**/*.rb"
|
11
|
+
- "**/*.yaml"
|
12
|
+
- "**/*.yml"
|
13
|
+
- "**/*.html"
|
14
|
+
fail:
|
15
|
+
- Signup via Github
|
16
|
+
pass:
|
17
|
+
- Signup via GitHub
|
18
|
+
EOC
|
19
|
+
|
20
|
+
attr_reader :stdout
|
21
|
+
attr_reader :stderr
|
22
|
+
attr_reader :path
|
23
|
+
attr_reader :force
|
24
|
+
|
25
|
+
def initialize(stdout:, stderr:, path:, force:)
|
26
|
+
@stdout = stdout
|
27
|
+
@stderr = stderr
|
28
|
+
@path = path
|
29
|
+
@force = force
|
30
|
+
end
|
31
|
+
|
32
|
+
def run
|
33
|
+
if path.file? && !force
|
34
|
+
stderr.puts "#{path} already exists. Try --force option to overwrite the file."
|
35
|
+
return 1
|
36
|
+
end
|
37
|
+
|
38
|
+
path.open("w") do |io|
|
39
|
+
io.print(CONFIG)
|
40
|
+
end
|
41
|
+
|
42
|
+
stdout.puts "Wrote #{path}. ✍️"
|
43
|
+
|
44
|
+
0
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
module Commands
|
3
|
+
class Test
|
4
|
+
include ConfigLoading
|
5
|
+
|
6
|
+
attr_reader :stdout
|
7
|
+
attr_reader :stderr
|
8
|
+
attr_reader :config_path
|
9
|
+
|
10
|
+
def initialize(stdout:, stderr:, config_path:)
|
11
|
+
@stdout = stdout
|
12
|
+
@stderr = stderr
|
13
|
+
@config_path = config_path
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
load_config!
|
18
|
+
|
19
|
+
validate_rule_uniqueness or return 1
|
20
|
+
validate_rules or return 1
|
21
|
+
|
22
|
+
0
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_rule_uniqueness
|
26
|
+
stdout.puts "Validating rule id uniqueness..."
|
27
|
+
|
28
|
+
duplicated_ids = []
|
29
|
+
|
30
|
+
config.rules.group_by(&:id).each do |id, rules|
|
31
|
+
if rules.size > 1
|
32
|
+
duplicated_ids << id
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if duplicated_ids.empty?
|
37
|
+
stdout.puts " OK!👍"
|
38
|
+
true
|
39
|
+
else
|
40
|
+
stdout.puts(Rainbow(" Found #{duplicated_ids.size} duplications.😞").red)
|
41
|
+
duplicated_ids.each do |id|
|
42
|
+
stdout.puts " #{id}"
|
43
|
+
end
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_rules
|
49
|
+
test_pass = true
|
50
|
+
|
51
|
+
config.rules.each do |rule|
|
52
|
+
if !rule.passes.empty? || !rule.fails.empty?
|
53
|
+
stdout.puts "Testing rule #{rule.id}..."
|
54
|
+
|
55
|
+
pass_errors = rule.passes.each.with_index.select do |pass, index|
|
56
|
+
rule_matches_example?(rule, pass)
|
57
|
+
end
|
58
|
+
|
59
|
+
fail_errors = rule.fails.each.with_index.reject do |fail, index|
|
60
|
+
rule_matches_example?(rule, fail)
|
61
|
+
end
|
62
|
+
|
63
|
+
unless pass_errors.empty?
|
64
|
+
test_pass = false
|
65
|
+
|
66
|
+
pass_errors.each do |_, index|
|
67
|
+
stdout.puts " #{(index+1).ordinalize} pass example matched.😱"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
unless fail_errors.empty?
|
72
|
+
test_pass = false
|
73
|
+
|
74
|
+
fail_errors.each do |_, index|
|
75
|
+
stdout.puts " #{(index+1).ordinalize} fail example didn't match.😱"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
if pass_errors.empty? && fail_errors.empty?
|
80
|
+
stdout.puts " OK!🎉"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
test_pass
|
86
|
+
end
|
87
|
+
|
88
|
+
def rule_matches_example?(rule, example)
|
89
|
+
buffer = Buffer.new(path: Pathname("-"), content: example)
|
90
|
+
analyzer = Analyzer.new(rule: rule, buffer: buffer)
|
91
|
+
analyzer.scan.count > 0
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
class Config
|
3
|
+
attr_reader :rules
|
4
|
+
|
5
|
+
def initialize(rules:)
|
6
|
+
@rules = rules
|
7
|
+
end
|
8
|
+
|
9
|
+
def rules_for_path(path, rules_filter:, &block)
|
10
|
+
if block_given?
|
11
|
+
rules.map do |rule|
|
12
|
+
if rules_filter.empty? || rules_filter.any? {|filter| /\A#{Regexp.escape(filter)}\.?/ =~ rule.id }
|
13
|
+
if rule.globs.empty?
|
14
|
+
[rule, nil]
|
15
|
+
else
|
16
|
+
glob = rule.globs.find {|glob| path.fnmatch?(glob.pattern, File::FNM_PATHNAME) }
|
17
|
+
if glob
|
18
|
+
[rule, glob]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end.compact.each(&block)
|
23
|
+
else
|
24
|
+
enum_for(:rules_for_path, path, rules_filter: rules_filter)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
class ConfigLoader
|
3
|
+
include ArrayHelper
|
4
|
+
|
5
|
+
class InvalidPattern < StandardError; end
|
6
|
+
|
7
|
+
Schema = StrongJSON.new do
|
8
|
+
let :regexp_pattern, object(regexp: string, case_insensitive: boolean?, multiline: boolean?)
|
9
|
+
let :literal_pattern, object(literal: string, case_insensitive: boolean?)
|
10
|
+
let :token_pattern, object(token: string)
|
11
|
+
let :pattern, enum(regexp_pattern, literal_pattern, token_pattern, string)
|
12
|
+
|
13
|
+
let :encoding, enum(*Encoding.name_list.map {|name| literal(name) })
|
14
|
+
let :glob, object(pattern: string, encoding: optional(encoding))
|
15
|
+
|
16
|
+
let :rule, object(
|
17
|
+
id: string,
|
18
|
+
pattern: enum(array(pattern), pattern),
|
19
|
+
message: string,
|
20
|
+
justification: optional(enum(array(string), string)),
|
21
|
+
glob: optional(enum(array(enum(glob, string)), glob, string)),
|
22
|
+
pass: optional(enum(array(string), string)),
|
23
|
+
fail: optional(enum(array(string), string))
|
24
|
+
)
|
25
|
+
|
26
|
+
let :rules, array(rule)
|
27
|
+
|
28
|
+
let :config, object(rules: rules)
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :path
|
32
|
+
attr_reader :content
|
33
|
+
|
34
|
+
def initialize(path:, content:)
|
35
|
+
@path = path
|
36
|
+
@content = content
|
37
|
+
end
|
38
|
+
|
39
|
+
def load
|
40
|
+
Schema.config.coerce(content)
|
41
|
+
rules = content[:rules].map {|hash| load_rule(hash) }
|
42
|
+
Config.new(rules: rules)
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_rule(hash)
|
46
|
+
id = hash[:id]
|
47
|
+
patterns = array(hash[:pattern]).map {|pat| load_pattern(pat) }
|
48
|
+
justifications = array(hash[:justification])
|
49
|
+
globs = load_globs(array(hash[:glob]))
|
50
|
+
message = hash[:message].chomp
|
51
|
+
passes = array(hash[:pass])
|
52
|
+
fails = array(hash[:fail])
|
53
|
+
|
54
|
+
Rule.new(id: id, patterns: patterns, justifications: justifications, globs: globs, message: message, passes: passes, fails: fails)
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_globs(globs)
|
58
|
+
globs.map do |glob|
|
59
|
+
case glob
|
60
|
+
when String
|
61
|
+
Glob.new(pattern: glob, encoding: nil)
|
62
|
+
when Hash
|
63
|
+
Glob.new(pattern: glob[:pattern], encoding: glob[:encoding])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_pattern(pattern)
|
69
|
+
case pattern
|
70
|
+
when String
|
71
|
+
Pattern.literal(pattern, case_insensitive: false)
|
72
|
+
when Hash
|
73
|
+
case
|
74
|
+
when pattern[:literal]
|
75
|
+
ci = pattern[:case_insensitive]
|
76
|
+
literal = pattern[:literal]
|
77
|
+
Pattern.literal(literal, case_insensitive: ci)
|
78
|
+
when pattern[:regexp]
|
79
|
+
regexp = pattern[:regexp]
|
80
|
+
ci = pattern[:case_insensitive]
|
81
|
+
multiline = pattern[:multiline]
|
82
|
+
Pattern.regexp(regexp, case_insensitive: ci, multiline: multiline)
|
83
|
+
when pattern[:token]
|
84
|
+
tok = pattern[:token]
|
85
|
+
Pattern.token(tok)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
class Issue
|
3
|
+
attr_reader :buffer
|
4
|
+
attr_reader :range
|
5
|
+
attr_reader :rule
|
6
|
+
attr_reader :text
|
7
|
+
|
8
|
+
def initialize(buffer:, range:, rule:, text:)
|
9
|
+
@buffer = buffer
|
10
|
+
@range = range
|
11
|
+
@rule = rule
|
12
|
+
@text = text
|
13
|
+
end
|
14
|
+
|
15
|
+
def path
|
16
|
+
buffer.path
|
17
|
+
end
|
18
|
+
|
19
|
+
def location
|
20
|
+
unless @location
|
21
|
+
start_line, start_column = buffer.location_for_position(range.begin)
|
22
|
+
end_line, end_column = buffer.location_for_position(range.end)
|
23
|
+
@location = Location.new(start_line: start_line, start_column: start_column, end_line: end_line, end_column: end_column)
|
24
|
+
end
|
25
|
+
|
26
|
+
@location
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
class Location
|
3
|
+
attr_reader :start_line
|
4
|
+
attr_reader :start_column
|
5
|
+
attr_reader :end_line
|
6
|
+
attr_reader :end_column
|
7
|
+
|
8
|
+
def initialize(start_line:, start_column:, end_line:, end_column:)
|
9
|
+
@start_line = start_line
|
10
|
+
@start_column = start_column
|
11
|
+
@end_line = end_line
|
12
|
+
@end_column = end_column
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
other.is_a?(Location) &&
|
17
|
+
other.start_line == start_line &&
|
18
|
+
other.start_column == start_column &&
|
19
|
+
other.end_line == end_line &&
|
20
|
+
other.end_column == end_column
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
class Matcher
|
3
|
+
attr_reader :path
|
4
|
+
attr_reader :src
|
5
|
+
attr_reader :rule
|
6
|
+
|
7
|
+
def initialize(path:, src:, rule:)
|
8
|
+
@path = path
|
9
|
+
@src = src
|
10
|
+
@rule = rule
|
11
|
+
end
|
12
|
+
|
13
|
+
def each
|
14
|
+
if block_given?
|
15
|
+
|
16
|
+
else
|
17
|
+
enum_for :each
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|