goodcheck 1.7.1 → 2.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +72 -8
- data/cheatsheet.pdf +0 -0
- data/goodcheck.gemspec +1 -1
- data/lib/goodcheck/analyzer.rb +66 -39
- data/lib/goodcheck/commands/check.rb +10 -6
- data/lib/goodcheck/commands/test.rb +42 -24
- data/lib/goodcheck/config.rb +11 -9
- data/lib/goodcheck/config_loader.rb +201 -9
- data/lib/goodcheck/glob.rb +6 -0
- data/lib/goodcheck/issue.rb +13 -0
- data/lib/goodcheck/pattern.rb +196 -55
- data/lib/goodcheck/rule.rb +3 -14
- data/lib/goodcheck/trigger.rb +44 -0
- data/lib/goodcheck/version.rb +1 -1
- data/lib/goodcheck.rb +1 -0
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1eb6ece8a435ad763555da38a18d8dacc04f382ae683f15fea06c7dbb117c23
|
4
|
+
data.tar.gz: ba146e257cc9dd9d1253af0009b59b75cd9063f40a8617a4454956a2c858c927
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8758e7b9ea900a80e044b1cc5cc8eafd00bc7063a2c821683c69bd6052a1d2418a127ad1e3a64863bb63833dda00597165401280d645b9514cd5909243818a1c
|
7
|
+
data.tar.gz: 4c7d2ef14d5c2fff6f49f89f94aa3b4b169625aafa4c5344a7af6ccdf77bc93cf5c79cdc11b0857f1ebc4951b1e8207d3173d67fb39872cd4a849946718ad992
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -36,6 +36,10 @@ The `init` command generates template of `goodcheck.yml` configuration file for
|
|
36
36
|
Edit the config file to define patterns you want to check.
|
37
37
|
Then run `check` command, and it will print matched texts.
|
38
38
|
|
39
|
+
### Cheatsheet
|
40
|
+
|
41
|
+
You can download a [printable cheatsheet](cheatsheet.pdf) from this repository.
|
42
|
+
|
39
43
|
## `goodcheck.yml`
|
40
44
|
|
41
45
|
An example of configuration is like the following:
|
@@ -73,7 +77,19 @@ The *rule* hash contains the following keys.
|
|
73
77
|
### *pattern*
|
74
78
|
|
75
79
|
A *pattern* can be a *literal pattern*, *regexp pattern*, *token pattern*, or a string.
|
76
|
-
|
80
|
+
|
81
|
+
#### String literal
|
82
|
+
|
83
|
+
String literal represents a *literal pattern* or *regexp pattern*.
|
84
|
+
|
85
|
+
```yaml
|
86
|
+
pattern:
|
87
|
+
- This is literal pattern
|
88
|
+
- /This is regexp pattern/
|
89
|
+
```
|
90
|
+
|
91
|
+
If the string value begins with `/` and ends with `/`, it is a *regexp pattern*.
|
92
|
+
You can optionally specify regexp options like `/casefold/i` or `/multiline/m`.
|
77
93
|
|
78
94
|
#### *literal pattern*
|
79
95
|
|
@@ -84,13 +100,11 @@ id: com.sample.GitHub
|
|
84
100
|
pattern:
|
85
101
|
literal: Github
|
86
102
|
case_sensitive: true
|
87
|
-
glob: []
|
88
103
|
message: Write GitHub, not Github
|
89
104
|
```
|
90
105
|
|
91
106
|
All regexp meta characters included in the `literal` value will be escaped.
|
92
107
|
`case_sensitive` is an optional key and the default is `true`.
|
93
|
-
`glob` is an optional key and the default is empty.
|
94
108
|
|
95
109
|
#### *regexp pattern*
|
96
110
|
|
@@ -102,13 +116,12 @@ pattern:
|
|
102
116
|
regexp: \d{4,}
|
103
117
|
case_sensitive: false
|
104
118
|
multiline: false
|
105
|
-
glob: []
|
106
119
|
message: Insert delimiters when writing large numbers
|
107
120
|
justification:
|
108
121
|
- When you are not writing numbers, including phone numbers, zip code, ...
|
109
122
|
```
|
110
123
|
|
111
|
-
It accepts two optional attributes, `case_sensitive
|
124
|
+
It accepts two optional attributes, `case_sensitive` and `multiline`.
|
112
125
|
The default values of `case_sensitive` and `multiline` are `true` and `false` respectively.
|
113
126
|
|
114
127
|
The regexp will be passed to `Regexp.compile`.
|
@@ -123,7 +136,6 @@ id: com.sample.no-blink
|
|
123
136
|
pattern:
|
124
137
|
token: "<blink"
|
125
138
|
case_sensitive: false
|
126
|
-
glob: []
|
127
139
|
message: Stop using <blink> tag
|
128
140
|
glob: "**/*.html"
|
129
141
|
justification:
|
@@ -137,10 +149,34 @@ In that case, try using *regexp pattern*.
|
|
137
149
|
The generated regexp of `<blink` is `<\s*blink\b/m`.
|
138
150
|
It matches with `<blink />` and `< BLINK>`, but does not match with `https://www.chromium.org/blink`.
|
139
151
|
|
140
|
-
It accepts one optional
|
152
|
+
It accepts one optional attribute `case_sensitive`.
|
141
153
|
The default value of `case_sensitive` is `true`.
|
142
154
|
Note that the generated regexp is in multiline mode.
|
143
155
|
|
156
|
+
Token pattern can have optional `where` attribute and *variable bindings*.
|
157
|
+
|
158
|
+
```yaml
|
159
|
+
pattern:
|
160
|
+
- token: bgcolor=${color:string}
|
161
|
+
where:
|
162
|
+
color: true
|
163
|
+
```
|
164
|
+
|
165
|
+
The variable binding consists of *variable name* and *variable type*, where `color` and `string` in the example above respectively. You have to add a key of the *variable name* in `where` attribute.
|
166
|
+
|
167
|
+
We have 8 builtin patterns:
|
168
|
+
|
169
|
+
* `string`
|
170
|
+
* `int`
|
171
|
+
* `float`
|
172
|
+
* `number`
|
173
|
+
* `url`
|
174
|
+
* `email`
|
175
|
+
* `word`
|
176
|
+
* `identifier`
|
177
|
+
|
178
|
+
You can find the exact definitions of the types in the definition of `Goodcheck::Pattern::Token` (`@@TYPES`).
|
179
|
+
|
144
180
|
### *glob*
|
145
181
|
|
146
182
|
A *glob* can be a string, or a hash.
|
@@ -172,7 +208,7 @@ rules:
|
|
172
208
|
glob: "*.txt"
|
173
209
|
```
|
174
210
|
|
175
|
-
### A rule
|
211
|
+
### A rule with _negated_ pattern
|
176
212
|
|
177
213
|
Goodcheck rules are usually to detect _something is included in a file_.
|
178
214
|
You can define the _negated_ rules for the opposite, _something is missing in a file_.
|
@@ -208,6 +244,34 @@ $ goodcheck check
|
|
208
244
|
db/schema.rb:-:# This file is auto-generated from the current state of the database. Instead: Read the operation manual for DB migration: https://example.com/guides/123
|
209
245
|
```
|
210
246
|
|
247
|
+
### Triggers
|
248
|
+
|
249
|
+
Version 2.0.0 introduces new abstruction to define patterns, trigger.
|
250
|
+
You can continue using `pattern`s in `rule`, but using `trigger` allows more flexible pattern definition and more precise testing.
|
251
|
+
|
252
|
+
```
|
253
|
+
rules:
|
254
|
+
- id: trigger
|
255
|
+
message: Using trigger
|
256
|
+
trigger:
|
257
|
+
- pattern: <blink
|
258
|
+
glob: "**/*.html"
|
259
|
+
fail:
|
260
|
+
- <blink></blink>
|
261
|
+
- not:
|
262
|
+
pattern:
|
263
|
+
token: <meta charset="UTF-8">
|
264
|
+
case_sensitive: false
|
265
|
+
glob: "**/*.html"
|
266
|
+
pass: |
|
267
|
+
<html>
|
268
|
+
<meta charset="utf-8"></meta>
|
269
|
+
</html>
|
270
|
+
```
|
271
|
+
|
272
|
+
You can continue existing `pattern` definitions, but `goodcheck test` against `pattern`s with `glob` does not work.
|
273
|
+
If your `pattern` definition includes `glob`, swithing `trigger` would make sense.
|
274
|
+
|
211
275
|
## Importing rules
|
212
276
|
|
213
277
|
`goodcheck.yml` can have optional `import` attribute.
|
data/cheatsheet.pdf
ADDED
Binary file
|
data/goodcheck.gemspec
CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_development_dependency "minitest-reporters", "~> 1.3.6"
|
29
29
|
|
30
30
|
spec.add_runtime_dependency "activesupport", ">= 4.0", "< 6.0"
|
31
|
-
spec.add_runtime_dependency "strong_json", "~> 1.0
|
31
|
+
spec.add_runtime_dependency "strong_json", "~> 1.1.0"
|
32
32
|
spec.add_runtime_dependency "rainbow", "~> 3.0.0"
|
33
33
|
spec.add_runtime_dependency "httpclient", "~> 2.8.3"
|
34
34
|
end
|
data/lib/goodcheck/analyzer.rb
CHANGED
@@ -1,66 +1,93 @@
|
|
1
1
|
module Goodcheck
|
2
2
|
class Analyzer
|
3
3
|
attr_reader :rule
|
4
|
+
attr_reader :trigger
|
4
5
|
attr_reader :buffer
|
5
6
|
|
6
|
-
def initialize(rule:, buffer:)
|
7
|
+
def initialize(rule:, trigger:, buffer:)
|
7
8
|
@rule = rule
|
9
|
+
@trigger = trigger
|
8
10
|
@buffer = buffer
|
9
11
|
end
|
10
12
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
13
|
+
def scan(&block)
|
14
|
+
if block_given?
|
15
|
+
if trigger.patterns.empty?
|
16
|
+
yield Issue.new(buffer: buffer, range: nil, rule: rule, text: nil)
|
17
|
+
else
|
18
|
+
var_pats, novar_pats = trigger.patterns.partition {|pat|
|
19
|
+
pat.is_a?(Pattern::Token) && !pat.variables.empty?
|
20
|
+
}
|
14
21
|
|
15
|
-
|
16
|
-
|
17
|
-
|
22
|
+
unless var_pats.empty?
|
23
|
+
var_pats.each do |pat|
|
24
|
+
scan_var pat, &block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
unless novar_pats.empty?
|
29
|
+
regexp = Regexp.union(*novar_pats.map(&:regexp))
|
30
|
+
scan_simple regexp, &block
|
31
|
+
end
|
32
|
+
end
|
18
33
|
else
|
19
|
-
|
34
|
+
enum_for(:scan)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def scan_simple(regexp, &block)
|
39
|
+
unless trigger.negated?
|
40
|
+
issues = []
|
41
|
+
|
42
|
+
scanner = StringScanner.new(buffer.content)
|
43
|
+
|
44
|
+
while true
|
20
45
|
case
|
21
|
-
when
|
22
|
-
|
23
|
-
|
24
|
-
|
46
|
+
when scanner.scan_until(regexp)
|
47
|
+
text = scanner.matched
|
48
|
+
range = (scanner.pos - text.bytesize) .. scanner.pos
|
49
|
+
issues << Issue.new(buffer: buffer, range: range, rule: rule, text: text)
|
25
50
|
else
|
26
|
-
|
51
|
+
break
|
27
52
|
end
|
28
53
|
end
|
29
|
-
end
|
30
|
-
end
|
31
54
|
|
32
|
-
|
33
|
-
|
34
|
-
|
55
|
+
issues.each(&block)
|
56
|
+
else
|
57
|
+
unless regexp =~ buffer.content
|
35
58
|
yield Issue.new(buffer: buffer, range: nil, rule: rule, text: nil)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
unless rule.negated?
|
40
|
-
issues = []
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
41
62
|
|
42
|
-
|
63
|
+
def scan_var(pat)
|
64
|
+
scanner = StringScanner.new(buffer.content)
|
43
65
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
66
|
+
unless trigger.negated?
|
67
|
+
while true
|
68
|
+
case
|
69
|
+
when scanner.scan_until(pat.regexp)
|
70
|
+
if pat.test_variables(scanner)
|
71
|
+
text = scanner.matched
|
72
|
+
range = (scanner.pos - text.bytesize) .. scanner.pos
|
73
|
+
yield Issue.new(buffer: buffer, range: range, rule: rule, text: text)
|
53
74
|
end
|
54
|
-
|
55
|
-
issues.each(&block)
|
56
75
|
else
|
57
|
-
|
58
|
-
yield Issue.new(buffer: buffer, range: nil, rule: rule, text: nil)
|
59
|
-
end
|
76
|
+
break
|
60
77
|
end
|
61
78
|
end
|
62
79
|
else
|
63
|
-
|
80
|
+
while true
|
81
|
+
case
|
82
|
+
when scanner.scan_until(pat.regexp)
|
83
|
+
if pat.test(scanner)
|
84
|
+
break
|
85
|
+
end
|
86
|
+
else
|
87
|
+
yield Issue.new(buffer: buffer, range: nil, rule: rule, text: nil)
|
88
|
+
break
|
89
|
+
end
|
90
|
+
end
|
64
91
|
end
|
65
92
|
end
|
66
93
|
end
|
@@ -28,12 +28,16 @@ module Goodcheck
|
|
28
28
|
|
29
29
|
reporter.analysis do
|
30
30
|
load_config!(force_download: force_download, cache_path: cache_dir_path)
|
31
|
-
each_check do |buffer, rule|
|
31
|
+
each_check do |buffer, rule, trigger|
|
32
|
+
reported_issues = Set[]
|
33
|
+
|
32
34
|
reporter.rule(rule) do
|
33
|
-
analyzer = Analyzer.new(rule: rule, buffer: buffer)
|
35
|
+
analyzer = Analyzer.new(rule: rule, buffer: buffer, trigger: trigger)
|
34
36
|
analyzer.scan do |issue|
|
35
|
-
|
36
|
-
|
37
|
+
if reported_issues.add?(issue)
|
38
|
+
issue_reported = true
|
39
|
+
reporter.issue(issue)
|
40
|
+
end
|
37
41
|
end
|
38
42
|
end
|
39
43
|
end
|
@@ -53,7 +57,7 @@ module Goodcheck
|
|
53
57
|
reporter.file(path) do
|
54
58
|
buffers = {}
|
55
59
|
|
56
|
-
config.rules_for_path(path, rules_filter: rules) do |rule, glob|
|
60
|
+
config.rules_for_path(path, rules_filter: rules) do |rule, glob, trigger|
|
57
61
|
Goodcheck.logger.debug "Checking rule: #{rule.id}"
|
58
62
|
begin
|
59
63
|
encoding = glob&.encoding || Encoding.default_external.name
|
@@ -66,7 +70,7 @@ module Goodcheck
|
|
66
70
|
buffers[encoding] = buffer
|
67
71
|
end
|
68
72
|
|
69
|
-
yield buffer, rule
|
73
|
+
yield buffer, rule, trigger
|
70
74
|
rescue ArgumentError => exn
|
71
75
|
stderr.puts "#{path}: #{exn.inspect}"
|
72
76
|
end
|
@@ -56,34 +56,53 @@ module Goodcheck
|
|
56
56
|
test_pass = true
|
57
57
|
|
58
58
|
config.rules.each do |rule|
|
59
|
-
if
|
59
|
+
if rule.triggers.any? {|trigger| !trigger.passes.empty? || !trigger.fails.empty?}
|
60
60
|
stdout.puts "Testing rule #{rule.id}..."
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
62
|
+
rule_ok = true
|
63
|
+
|
64
|
+
rule.triggers.each.with_index do |trigger, index|
|
65
|
+
if !trigger.passes.empty? || !trigger.fails.empty?
|
66
|
+
if trigger.by_pattern?
|
67
|
+
stdout.puts " Testing pattern..."
|
68
|
+
else
|
69
|
+
stdout.puts " Testing #{(index+1).ordinalize} trigger..."
|
70
|
+
end
|
71
|
+
|
72
|
+
pass_errors = trigger.passes.each.with_index.select do |pass, _|
|
73
|
+
rule_matches_example?(rule, trigger, pass)
|
74
|
+
end
|
75
|
+
|
76
|
+
fail_errors = trigger.fails.each.with_index.reject do |fail, _|
|
77
|
+
rule_matches_example?(rule, trigger, fail)
|
78
|
+
end
|
79
|
+
|
80
|
+
unless pass_errors.empty?
|
81
|
+
test_pass = false
|
82
|
+
rule_ok = false
|
83
|
+
|
84
|
+
pass_errors.each do |_, index|
|
85
|
+
stdout.puts " #{(index+1).ordinalize} pass example matched.😱"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
unless fail_errors.empty?
|
90
|
+
test_pass = false
|
91
|
+
rule_ok = false
|
92
|
+
|
93
|
+
fail_errors.each do |_, index|
|
94
|
+
stdout.puts " #{(index+1).ordinalize} fail example didn't match.😱"
|
95
|
+
end
|
96
|
+
end
|
75
97
|
end
|
76
98
|
end
|
77
99
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
fail_errors.each do |_, index|
|
82
|
-
stdout.puts " #{(index+1).ordinalize} fail example didn't match.😱"
|
83
|
-
end
|
100
|
+
if rule.triggers.any?(&:skips_fail_examples?)
|
101
|
+
stdout.puts " 🚨 The rule contains a `pattern` with `glob`, which is not supported by the test command."
|
102
|
+
stdout.puts " Skips testing `fail` examples."
|
84
103
|
end
|
85
104
|
|
86
|
-
if
|
105
|
+
if rule_ok
|
87
106
|
stdout.puts " OK!🎉"
|
88
107
|
end
|
89
108
|
end
|
@@ -92,10 +111,9 @@ module Goodcheck
|
|
92
111
|
test_pass
|
93
112
|
end
|
94
113
|
|
95
|
-
def rule_matches_example?(rule, example)
|
114
|
+
def rule_matches_example?(rule, trigger, example)
|
96
115
|
buffer = Buffer.new(path: Pathname("-"), content: example)
|
97
|
-
analyzer = Analyzer.new(rule: rule, buffer: buffer)
|
98
|
-
analyzer.use_all_patterns!
|
116
|
+
analyzer = Analyzer.new(rule: rule, buffer: buffer, trigger: trigger)
|
99
117
|
analyzer.scan.count > 0
|
100
118
|
end
|
101
119
|
end
|
data/lib/goodcheck/config.rb
CHANGED
@@ -24,20 +24,22 @@ module Goodcheck
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
def rules_for_path(path, rules_filter
|
27
|
+
def rules_for_path(path, rules_filter:)
|
28
28
|
if block_given?
|
29
29
|
each_rule(filter: rules_filter).map do |rule|
|
30
|
-
|
30
|
+
rule.triggers.each do |trigger|
|
31
|
+
globs = trigger.globs
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
33
|
+
if globs.empty?
|
34
|
+
yield [rule, nil, trigger]
|
35
|
+
else
|
36
|
+
glob = globs.find {|glob| glob.test(path) }
|
37
|
+
if glob
|
38
|
+
yield [rule, glob, trigger]
|
39
|
+
end
|
38
40
|
end
|
39
41
|
end
|
40
|
-
end
|
42
|
+
end
|
41
43
|
else
|
42
44
|
enum_for(:rules_for_path, path, rules_filter: rules_filter)
|
43
45
|
end
|
@@ -35,9 +35,36 @@ module Goodcheck
|
|
35
35
|
})
|
36
36
|
let :glob, array_or(one_glob)
|
37
37
|
|
38
|
+
let :var_pattern, any
|
39
|
+
let :variable_pattern, array_or(var_pattern)
|
40
|
+
let :negated_variable_pattern, object(not: variable_pattern)
|
41
|
+
|
42
|
+
let :where, hash(
|
43
|
+
enum(
|
44
|
+
variable_pattern,
|
45
|
+
negated_variable_pattern,
|
46
|
+
literal(true),
|
47
|
+
detector: -> (value) {
|
48
|
+
case
|
49
|
+
when value.is_a?(Hash) && value.key?(:not)
|
50
|
+
negated_variable_pattern
|
51
|
+
when value == true
|
52
|
+
literal(true)
|
53
|
+
else
|
54
|
+
variable_pattern
|
55
|
+
end
|
56
|
+
}
|
57
|
+
)
|
58
|
+
)
|
59
|
+
|
38
60
|
let :regexp_pattern, object(regexp: string, case_sensitive: boolean?, multiline: boolean?, glob: optional(glob))
|
39
61
|
let :literal_pattern, object(literal: string, case_sensitive: boolean?, glob: optional(glob))
|
40
|
-
let :token_pattern, object(
|
62
|
+
let :token_pattern, object(
|
63
|
+
token: string,
|
64
|
+
case_sensitive: boolean?,
|
65
|
+
glob: optional(glob),
|
66
|
+
where: optional(where)
|
67
|
+
)
|
41
68
|
|
42
69
|
let :pattern, enum(regexp_pattern,
|
43
70
|
literal_pattern,
|
@@ -95,12 +122,58 @@ module Goodcheck
|
|
95
122
|
glob: glob
|
96
123
|
)
|
97
124
|
|
125
|
+
let :positive_trigger, object(
|
126
|
+
pattern: array_or(pattern),
|
127
|
+
glob: optional(glob),
|
128
|
+
pass: optional(array_or(string)),
|
129
|
+
fail: optional(array_or(string))
|
130
|
+
)
|
131
|
+
|
132
|
+
let :negative_trigger, object(
|
133
|
+
not: object(pattern: array_or(pattern)),
|
134
|
+
glob: optional(glob),
|
135
|
+
pass: optional(array_or(string)),
|
136
|
+
fail: optional(array_or(string))
|
137
|
+
)
|
138
|
+
|
139
|
+
let :nopattern_trigger, object(
|
140
|
+
glob: glob_obj
|
141
|
+
)
|
142
|
+
|
143
|
+
let :trigger, enum(
|
144
|
+
positive_trigger,
|
145
|
+
negative_trigger,
|
146
|
+
nopattern_trigger,
|
147
|
+
detector: -> (hash) {
|
148
|
+
if hash.is_a?(Hash)
|
149
|
+
case
|
150
|
+
when hash.key?(:pattern)
|
151
|
+
positive_trigger
|
152
|
+
when hash.key?(:not)
|
153
|
+
negative_trigger
|
154
|
+
else
|
155
|
+
nopattern_trigger
|
156
|
+
end
|
157
|
+
end
|
158
|
+
}
|
159
|
+
)
|
160
|
+
|
161
|
+
let :triggered_rule, object(
|
162
|
+
id: string,
|
163
|
+
message: string,
|
164
|
+
justification: optional(array_or(string)),
|
165
|
+
trigger: array_or(trigger)
|
166
|
+
)
|
167
|
+
|
98
168
|
let :rule, enum(positive_rule,
|
99
169
|
negative_rule,
|
100
170
|
nopattern_rule,
|
171
|
+
triggered_rule,
|
101
172
|
detector: -> (hash) {
|
102
173
|
if hash.is_a?(Hash)
|
103
174
|
case
|
175
|
+
when hash[:trigger]
|
176
|
+
triggered_rule
|
104
177
|
when hash[:pattern]
|
105
178
|
positive_rule
|
106
179
|
when hash[:not]
|
@@ -180,14 +253,76 @@ module Goodcheck
|
|
180
253
|
Goodcheck.logger.debug "Loading rule: #{hash[:id]}"
|
181
254
|
|
182
255
|
id = hash[:id]
|
183
|
-
|
256
|
+
triggers = retrieve_triggers(hash)
|
184
257
|
justifications = array(hash[:justification])
|
185
|
-
globs = load_globs(array(hash[:glob]))
|
186
258
|
message = hash[:message].chomp
|
259
|
+
|
260
|
+
Rule.new(id: id, message: message, justifications: justifications, triggers: triggers)
|
261
|
+
end
|
262
|
+
|
263
|
+
def retrieve_triggers(hash)
|
264
|
+
if hash.key?(:trigger)
|
265
|
+
array(hash[:trigger]).map do |trigger|
|
266
|
+
retrieve_trigger(trigger)
|
267
|
+
end
|
268
|
+
else
|
269
|
+
globs = load_globs(array(hash[:glob]))
|
270
|
+
passes = array(hash[:pass])
|
271
|
+
fails = array(hash[:fail])
|
272
|
+
|
273
|
+
if hash.key?(:not) || hash.key?(:pattern)
|
274
|
+
if hash.key?(:not)
|
275
|
+
negated = true
|
276
|
+
patterns = array(hash[:not][:pattern])
|
277
|
+
else
|
278
|
+
negated = false
|
279
|
+
patterns = array(hash[:pattern])
|
280
|
+
end
|
281
|
+
|
282
|
+
glob_patterns, noglob_patterns = patterns.partition {|pat|
|
283
|
+
pat.is_a?(Hash) && pat.key?(:glob)
|
284
|
+
}
|
285
|
+
|
286
|
+
skip_fails = !fails.empty? && !glob_patterns.empty?
|
287
|
+
|
288
|
+
glob_patterns.map do |pat|
|
289
|
+
Trigger.new(
|
290
|
+
patterns: [load_pattern(pat)],
|
291
|
+
globs: load_globs(array(pat[:glob])),
|
292
|
+
passes: passes,
|
293
|
+
fails: [],
|
294
|
+
negated: negated
|
295
|
+
).by_pattern!.skips_fail_examples!(skip_fails)
|
296
|
+
end.push(
|
297
|
+
Trigger.new(
|
298
|
+
patterns: noglob_patterns.map {|pat| load_pattern(pat) },
|
299
|
+
globs: globs,
|
300
|
+
passes: passes,
|
301
|
+
fails: glob_patterns.empty? ? fails : [],
|
302
|
+
negated: negated
|
303
|
+
).by_pattern!.skips_fail_examples!(skip_fails)
|
304
|
+
)
|
305
|
+
else
|
306
|
+
[Trigger.new(patterns: [],
|
307
|
+
globs: globs,
|
308
|
+
passes: passes,
|
309
|
+
fails: fails,
|
310
|
+
negated: false).by_pattern!]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def retrieve_trigger(hash)
|
316
|
+
patterns, negated = retrieve_patterns(hash)
|
317
|
+
globs = load_globs(array(hash[:glob]))
|
187
318
|
passes = array(hash[:pass])
|
188
319
|
fails = array(hash[:fail])
|
189
320
|
|
190
|
-
|
321
|
+
Trigger.new(patterns: patterns,
|
322
|
+
globs: globs,
|
323
|
+
passes: passes,
|
324
|
+
fails: fails,
|
325
|
+
negated: negated)
|
191
326
|
end
|
192
327
|
|
193
328
|
def retrieve_patterns(hash)
|
@@ -219,27 +354,84 @@ module Goodcheck
|
|
219
354
|
def load_pattern(pattern)
|
220
355
|
case pattern
|
221
356
|
when String
|
222
|
-
|
357
|
+
case (pat = load_string_pattern(pattern))
|
358
|
+
when String
|
359
|
+
Pattern::Literal.new(source: pat, case_sensitive: true)
|
360
|
+
when ::Regexp
|
361
|
+
Pattern::Regexp.new(source: pattern,
|
362
|
+
regexp: pat,
|
363
|
+
multiline: pat.multiline?,
|
364
|
+
case_sensitive: !pat.casefold?)
|
365
|
+
end
|
223
366
|
when Hash
|
224
|
-
|
367
|
+
if pattern[:glob]
|
368
|
+
print_warning_once "🌏 Pattern with glob is deprecated: globs are ignored at all."
|
369
|
+
end
|
370
|
+
|
225
371
|
case
|
226
372
|
when pattern[:literal]
|
227
373
|
cs = case_sensitive?(pattern)
|
228
374
|
literal = pattern[:literal]
|
229
|
-
Pattern.
|
375
|
+
Pattern::Literal.new(source: literal, case_sensitive: cs)
|
230
376
|
when pattern[:regexp]
|
231
377
|
regexp = pattern[:regexp]
|
232
378
|
cs = case_sensitive?(pattern)
|
233
379
|
multiline = pattern[:multiline]
|
234
|
-
Pattern.
|
380
|
+
Pattern::Regexp.new(source: regexp, case_sensitive: cs, multiline: multiline)
|
235
381
|
when pattern[:token]
|
236
382
|
tok = pattern[:token]
|
237
383
|
cs = case_sensitive?(pattern)
|
238
|
-
Pattern.
|
384
|
+
Pattern::Token.new(source: tok, variables: load_token_vars(pattern[:where]), case_sensitive: cs)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def load_string_pattern(string)
|
390
|
+
if string =~ /\A\/(.*)\/([im]*)\Z/
|
391
|
+
source = $1
|
392
|
+
opts = $2
|
393
|
+
options = 0
|
394
|
+
options |= ::Regexp::IGNORECASE if opts =~ /i/
|
395
|
+
options |= ::Regexp::MULTILINE if opts =~ /m/
|
396
|
+
::Regexp.new(source, options)
|
397
|
+
else
|
398
|
+
string
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
def load_token_vars(pattern)
|
403
|
+
case pattern
|
404
|
+
when Hash
|
405
|
+
pattern.each.with_object({}) do |(key, value), hash|
|
406
|
+
hash[key.to_sym] = load_var_pattern(value)
|
239
407
|
end
|
408
|
+
else
|
409
|
+
{}
|
240
410
|
end
|
241
411
|
end
|
242
412
|
|
413
|
+
def load_var_pattern(pattern)
|
414
|
+
if pattern.is_a?(Hash) && pattern[:not]
|
415
|
+
negated = true
|
416
|
+
pattern = pattern[:not]
|
417
|
+
else
|
418
|
+
negated = false
|
419
|
+
end
|
420
|
+
|
421
|
+
pattern = [] if pattern == true
|
422
|
+
|
423
|
+
patterns = array(pattern).map do |pat|
|
424
|
+
case pat
|
425
|
+
when String
|
426
|
+
load_string_pattern(pat)
|
427
|
+
else
|
428
|
+
pat
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
Pattern::Token::VarPattern.new(patterns: patterns, negated: negated)
|
433
|
+
end
|
434
|
+
|
243
435
|
def case_sensitive?(pattern)
|
244
436
|
return true if pattern.is_a?(String)
|
245
437
|
case
|
data/lib/goodcheck/glob.rb
CHANGED
data/lib/goodcheck/issue.rb
CHANGED
@@ -27,5 +27,18 @@ module Goodcheck
|
|
27
27
|
@location
|
28
28
|
end
|
29
29
|
end
|
30
|
+
|
31
|
+
def ==(other)
|
32
|
+
other.is_a?(Issue) &&
|
33
|
+
other.buffer == buffer &&
|
34
|
+
other.range == range &&
|
35
|
+
other.rule == rule
|
36
|
+
end
|
37
|
+
|
38
|
+
alias eql? ==
|
39
|
+
|
40
|
+
def hash
|
41
|
+
self.class.hash ^ buffer.hash ^ range.hash ^ rule.hash
|
42
|
+
end
|
30
43
|
end
|
31
44
|
end
|
data/lib/goodcheck/pattern.rb
CHANGED
@@ -1,74 +1,215 @@
|
|
1
1
|
module Goodcheck
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
2
|
+
module Pattern
|
3
|
+
class Literal
|
4
|
+
attr_reader :source
|
5
|
+
attr_reader :case_sensitive
|
6
|
+
|
7
|
+
def initialize(source:, case_sensitive:)
|
8
|
+
@source = source
|
9
|
+
@case_sensitive = case_sensitive
|
10
|
+
end
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
regexp: Regexp.compile(Regexp.escape(literal), !case_sensitive),
|
17
|
-
globs: globs
|
18
|
-
)
|
12
|
+
def regexp
|
13
|
+
@regexp ||= ::Regexp.compile(::Regexp.escape(source), !case_sensitive)
|
14
|
+
end
|
19
15
|
end
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
class Regexp
|
18
|
+
attr_reader :source
|
19
|
+
attr_reader :case_sensitive
|
20
|
+
attr_reader :multiline
|
25
21
|
|
26
|
-
|
27
|
-
source
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
22
|
+
def initialize(source:, case_sensitive:, multiline:, regexp: nil)
|
23
|
+
@source = source
|
24
|
+
@case_sensitive = case_sensitive
|
25
|
+
@multiline = multiline
|
26
|
+
@regexp = regexp
|
27
|
+
end
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
29
|
+
def regexp
|
30
|
+
@regexp ||= begin
|
31
|
+
options = 0
|
32
|
+
options |= ::Regexp::IGNORECASE unless case_sensitive
|
33
|
+
options |= ::Regexp::MULTILINE if multiline
|
34
|
+
::Regexp.compile(source, options)
|
35
|
+
end
|
36
|
+
end
|
39
37
|
end
|
40
38
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
39
|
+
class Token
|
40
|
+
attr_reader :source, :case_sensitive, :variables
|
41
|
+
|
42
|
+
def initialize(source:, variables:, case_sensitive:)
|
43
|
+
@source = source
|
44
|
+
@variables = variables
|
45
|
+
@case_sensitive = case_sensitive
|
46
|
+
end
|
47
|
+
|
48
|
+
def regexp
|
49
|
+
@regexp ||= Token.compile_tokens(source, variables, case_sensitive: case_sensitive)
|
50
|
+
end
|
51
|
+
|
52
|
+
class VarPattern
|
53
|
+
attr_reader :negated
|
54
|
+
attr_reader :patterns
|
55
|
+
attr_accessor :type
|
56
|
+
|
57
|
+
def initialize(patterns:, negated:)
|
58
|
+
@patterns = patterns
|
59
|
+
@negated = negated
|
60
|
+
end
|
61
|
+
|
62
|
+
def cast(str)
|
63
|
+
case type
|
64
|
+
when :int
|
65
|
+
str.to_i
|
66
|
+
when :float, :number
|
67
|
+
str.to_f
|
68
|
+
else
|
69
|
+
str
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def test(str)
|
74
|
+
return true if patterns.empty?
|
75
|
+
|
76
|
+
value = cast(str)
|
77
|
+
|
78
|
+
unless negated
|
79
|
+
patterns.any? {|pattern| pattern === value }
|
80
|
+
else
|
81
|
+
patterns.none? {|pattern| pattern === value }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.empty
|
86
|
+
VarPattern.new(patterns: [], negated: false)
|
57
87
|
end
|
58
88
|
end
|
59
89
|
|
60
|
-
|
61
|
-
|
90
|
+
def test_variables(match)
|
91
|
+
variables.all? do |name, var|
|
92
|
+
str = match[name]
|
93
|
+
str && var.test(str)
|
94
|
+
end
|
62
95
|
end
|
63
96
|
|
64
|
-
|
65
|
-
|
97
|
+
@@TYPES = {}
|
98
|
+
|
99
|
+
@@TYPES[:string] = -> (name) {
|
100
|
+
::Regexp.union(
|
101
|
+
/"(?<#{name}>(?:[^"]|\")*)"/,
|
102
|
+
/'(?<#{name}>(?:[^']|\')*)'/
|
103
|
+
)
|
104
|
+
}
|
105
|
+
|
106
|
+
@@TYPES[:number] = -> (name) {
|
107
|
+
::Regexp.union(
|
108
|
+
regexp_for_type(name: name, type: :int),
|
109
|
+
regexp_for_type(name: name, type: :float)
|
110
|
+
)
|
111
|
+
}
|
112
|
+
|
113
|
+
@@TYPES[:int] = -> (name) {
|
114
|
+
::Regexp.union(
|
115
|
+
/(?<#{name}>[+-]?[1-9](:?\d|_\d)*)/,
|
116
|
+
/(?<#{name}>[+-]?0[dD][0-7]+)/,
|
117
|
+
/(?<#{name}>[+-]?0[oO]?[0-7]+)/,
|
118
|
+
/(?<#{name}>[+-]?0[xX][0-9a-fA-F]+)/,
|
119
|
+
/(?<#{name}>[+-]?0[bB][01]+)/
|
120
|
+
)
|
121
|
+
}
|
122
|
+
|
123
|
+
@@TYPES[:float] = -> (name) {
|
124
|
+
::Regexp.union(
|
125
|
+
/(?<#{name}>[+-]?\d+\.\d*(:?e[+-]?\d+)?)/,
|
126
|
+
/(?<#{name}>[+-]?\d+(:?e[+-]?\d+)?)/
|
127
|
+
)
|
128
|
+
}
|
129
|
+
|
130
|
+
@@TYPES[:word] = -> (name) {
|
131
|
+
/(?<#{name}>\S+)/
|
132
|
+
}
|
133
|
+
|
134
|
+
@@TYPES[:identifier] = -> (name) {
|
135
|
+
/(?<#{name}>[a-zA-Z_]\w*)\b/
|
136
|
+
}
|
137
|
+
|
138
|
+
# From rails_autolink gem
|
139
|
+
# https://github.com/tenderlove/rails_autolink/blob/master/lib/rails_autolink/helpers.rb#L73
|
140
|
+
# With ')' support, which should be frequently used for markdown or CSS `url(...)`
|
141
|
+
AUTO_LINK_RE = %r{
|
142
|
+
(?: ((?:ed2k|ftp|http|https|irc|mailto|news|gopher|nntp|telnet|webcal|xmpp|callto|feed|svn|urn|aim|rsync|tag|ssh|sftp|rtsp|afs|file):)// | www\. )
|
143
|
+
[^\s<\u00A0")]+
|
144
|
+
}ix
|
145
|
+
|
146
|
+
# https://github.com/tenderlove/rails_autolink/blob/master/lib/rails_autolink/helpers.rb#L81-L82
|
147
|
+
AUTO_EMAIL_LOCAL_RE = /[\w.!#\$%&'*\/=?^`{|}~+-]/
|
148
|
+
AUTO_EMAIL_RE = /(?<!#{AUTO_EMAIL_LOCAL_RE})[\w.!#\$%+-]\.?#{AUTO_EMAIL_LOCAL_RE}*@[\w-]+(?:\.[\w-]+)+/
|
149
|
+
|
150
|
+
@@TYPES[:url] = -> (name) {
|
151
|
+
/\b(?<#{name}>#{AUTO_LINK_RE})/
|
152
|
+
}
|
153
|
+
|
154
|
+
@@TYPES[:email] = -> (name) {
|
155
|
+
/\b(?<#{name}>#{AUTO_EMAIL_RE})/
|
156
|
+
}
|
157
|
+
|
158
|
+
def self.regexp_for_type(name:, type:)
|
159
|
+
ty = type || :word
|
160
|
+
if @@TYPES.key?(ty)
|
161
|
+
@@TYPES[ty][name]
|
162
|
+
end
|
66
163
|
end
|
67
164
|
|
68
|
-
|
69
|
-
|
165
|
+
def self.compile_tokens(source, variables, case_sensitive:)
|
166
|
+
tokens = []
|
167
|
+
s = StringScanner.new(source)
|
168
|
+
|
169
|
+
until s.eos?
|
170
|
+
case
|
171
|
+
when s.scan(/\${(?<name>[a-zA-Z_]\w*)(?::(?<type>#{::Regexp.union(*@@TYPES.keys.map(&:to_s))}))?}/)
|
172
|
+
name = s[:name].to_sym
|
173
|
+
type = s[:type] && s[:type].to_sym
|
70
174
|
|
71
|
-
|
175
|
+
if variables.key?(name)
|
176
|
+
variables[name].type = type
|
177
|
+
regexp = regexp_for_type(name: name, type: type).to_s
|
178
|
+
if tokens.empty? && (type == :word || type == :identifier)
|
179
|
+
regexp = /\b#{regexp.to_s}/
|
180
|
+
end
|
181
|
+
tokens << regexp.to_s
|
182
|
+
else
|
183
|
+
tokens << ::Regexp.escape("${")
|
184
|
+
tokens << ::Regexp.escape(name.to_s)
|
185
|
+
tokens << ::Regexp.escape("}")
|
186
|
+
end
|
187
|
+
when s.scan(/\(|\)|\{|\}|\[|\]|\<|\>/)
|
188
|
+
tokens << ::Regexp.escape(s.matched)
|
189
|
+
when s.scan(/\s+/)
|
190
|
+
tokens << '\s+'
|
191
|
+
when s.scan(/\w+|[\p{L}&&\p{^ASCII}]+/)
|
192
|
+
tokens << ::Regexp.escape(s.matched)
|
193
|
+
when s.scan(%r{[!"#%&'=\-^~¥\\|`@*:+;/?.,]+})
|
194
|
+
tokens << ::Regexp.escape(s.matched.rstrip)
|
195
|
+
when s.scan(/./)
|
196
|
+
tokens << ::Regexp.escape(s.matched)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
if tokens.first =~ /\A\p{L}/
|
201
|
+
tokens.first.prepend('\b')
|
202
|
+
end
|
203
|
+
|
204
|
+
if tokens.last =~ /\p{L}\Z/
|
205
|
+
tokens.last << '\b'
|
206
|
+
end
|
207
|
+
|
208
|
+
options = ::Regexp::MULTILINE
|
209
|
+
options |= ::Regexp::IGNORECASE unless case_sensitive
|
210
|
+
|
211
|
+
::Regexp.new(tokens.join('\s*').gsub(/\\s\*(\\s\+\\s\*)+/, '\s+'), options)
|
212
|
+
end
|
72
213
|
end
|
73
214
|
end
|
74
215
|
end
|
data/lib/goodcheck/rule.rb
CHANGED
@@ -1,26 +1,15 @@
|
|
1
1
|
module Goodcheck
|
2
2
|
class Rule
|
3
3
|
attr_reader :id
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :triggers
|
5
5
|
attr_reader :message
|
6
6
|
attr_reader :justifications
|
7
|
-
attr_reader :globs
|
8
|
-
attr_reader :passes
|
9
|
-
attr_reader :fails
|
10
7
|
|
11
|
-
def initialize(id:,
|
8
|
+
def initialize(id:, triggers:, message:, justifications:)
|
12
9
|
@id = id
|
13
|
-
@
|
10
|
+
@triggers = triggers
|
14
11
|
@message = message
|
15
12
|
@justifications = justifications
|
16
|
-
@globs = globs
|
17
|
-
@passes = passes
|
18
|
-
@fails = fails
|
19
|
-
@negated = negated
|
20
|
-
end
|
21
|
-
|
22
|
-
def negated?
|
23
|
-
@negated
|
24
13
|
end
|
25
14
|
end
|
26
15
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Goodcheck
|
2
|
+
class Trigger
|
3
|
+
attr_reader :patterns
|
4
|
+
attr_reader :globs
|
5
|
+
attr_reader :passes
|
6
|
+
attr_reader :fails
|
7
|
+
attr_reader :negated
|
8
|
+
|
9
|
+
def initialize(patterns:, globs:, passes:, fails:, negated:)
|
10
|
+
@patterns = patterns
|
11
|
+
@globs = globs
|
12
|
+
@passes = passes
|
13
|
+
@fails = fails
|
14
|
+
@negated = negated
|
15
|
+
end
|
16
|
+
|
17
|
+
def by_pattern!
|
18
|
+
@by_pattern = true
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def by_pattern?
|
23
|
+
# True if the trigger is from `pattern` or `not` attribute (compatible mode.)
|
24
|
+
@by_pattern
|
25
|
+
end
|
26
|
+
|
27
|
+
def skips_fail_examples!(flag = true)
|
28
|
+
@skips_fail_examples = flag
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def skips_fail_examples?
|
33
|
+
@skips_fail_examples
|
34
|
+
end
|
35
|
+
|
36
|
+
def negated?
|
37
|
+
@negated
|
38
|
+
end
|
39
|
+
|
40
|
+
def fires_for?(path:)
|
41
|
+
globs.any? {|glob| glob.test(path) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/goodcheck/version.rb
CHANGED
data/lib/goodcheck.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: goodcheck
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Soutaro Matsumoto
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -92,14 +92,14 @@ dependencies:
|
|
92
92
|
requirements:
|
93
93
|
- - "~>"
|
94
94
|
- !ruby/object:Gem::Version
|
95
|
-
version: 1.0
|
95
|
+
version: 1.1.0
|
96
96
|
type: :runtime
|
97
97
|
prerelease: false
|
98
98
|
version_requirements: !ruby/object:Gem::Requirement
|
99
99
|
requirements:
|
100
100
|
- - "~>"
|
101
101
|
- !ruby/object:Gem::Version
|
102
|
-
version: 1.0
|
102
|
+
version: 1.1.0
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: rainbow
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -147,6 +147,7 @@ files:
|
|
147
147
|
- Rakefile
|
148
148
|
- bin/console
|
149
149
|
- bin/setup
|
150
|
+
- cheatsheet.pdf
|
150
151
|
- exe/goodcheck
|
151
152
|
- goodcheck.gemspec
|
152
153
|
- lib/goodcheck.rb
|
@@ -170,6 +171,7 @@ files:
|
|
170
171
|
- lib/goodcheck/reporters/json.rb
|
171
172
|
- lib/goodcheck/reporters/text.rb
|
172
173
|
- lib/goodcheck/rule.rb
|
174
|
+
- lib/goodcheck/trigger.rb
|
173
175
|
- lib/goodcheck/version.rb
|
174
176
|
- logo/GoodCheck Horizontal.pdf
|
175
177
|
- logo/GoodCheck Horizontal.png
|