goodcheck 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,11 @@
1
+ module Goodcheck
2
+ class Glob
3
+ attr_reader :pattern
4
+ attr_reader :encoding
5
+
6
+ def initialize(pattern:, encoding:)
7
+ @pattern = pattern
8
+ @encoding = encoding
9
+ end
10
+ end
11
+ 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