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