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.
@@ -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