goodcheck 1.7.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d857a24ca9b2ca50fe761dad0fa03fbeab34a7bf2c2d34eb6cdbeb349f31f53f
4
- data.tar.gz: d50e1f2b04239b282eaa1a89eb8965f68de2ff681b1ba814d6695f0491cd497f
3
+ metadata.gz: c1eb6ece8a435ad763555da38a18d8dacc04f382ae683f15fea06c7dbb117c23
4
+ data.tar.gz: ba146e257cc9dd9d1253af0009b59b75cd9063f40a8617a4454956a2c858c927
5
5
  SHA512:
6
- metadata.gz: f64e084cb6b268d3d3febf63a37b811830b3ef77318e73e0f049fb4fbae2c8bec97b8e339562042b2e8029f1035bb20a86ba03086366ac8caaa57bc418521061
7
- data.tar.gz: 7b8c73bd327db455afd2a61c74f0b4b6589e4c71c4c0205f60a71f3be37666f826832b608f4f0b0016fa6cdd905a62a90ab08814aa490d5e21de44a9543eee00
6
+ metadata.gz: 8758e7b9ea900a80e044b1cc5cc8eafd00bc7063a2c821683c69bd6052a1d2418a127ad1e3a64863bb63833dda00597165401280d645b9514cd5909243818a1c
7
+ data.tar.gz: 4c7d2ef14d5c2fff6f49f89f94aa3b4b169625aafa4c5344a7af6ccdf77bc93cf5c79cdc11b0857f1ebc4951b1e8207d3173d67fb39872cd4a849946718ad992
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 2.0.0 (2019-06-06)
6
+
7
+ * Introduce trigger, a new pattern definition [#53](https://github.com/sider/goodcheck/pull/53)
8
+
5
9
  ## 1.7.1 (2019-05-29)
6
10
 
7
11
  * Fix test command error handling
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
- When a string is given, it is interpreted as a *literal pattern* with `case_sensitive: true`.
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`, `multiline`, and `glob`.
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 attributes, `case_sensitive` and `glob`.
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 without _negated_ pattern
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.1"
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
@@ -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 use_all_patterns!
12
- @use_all_patterns = true
13
- end
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
- def patterns
16
- if @use_all_patterns
17
- rule.patterns
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
- rule.patterns.select do |pattern|
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 pattern.globs.empty? && rule.globs.empty?
22
- true
23
- when pattern.globs.empty?
24
- rule.globs.any? {|glob| glob.test(buffer.path) }
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
- pattern.globs.any? {|glob| glob.test(buffer.path) }
51
+ break
27
52
  end
28
53
  end
29
- end
30
- end
31
54
 
32
- def scan(&block)
33
- if block_given?
34
- if rule.patterns.empty?
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
- else
37
- regexp = Regexp.union(*patterns.map(&:regexp))
38
-
39
- unless rule.negated?
40
- issues = []
59
+ end
60
+ end
61
+ end
41
62
 
42
- scanner = StringScanner.new(buffer.content)
63
+ def scan_var(pat)
64
+ scanner = StringScanner.new(buffer.content)
43
65
 
44
- while true
45
- case
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)
50
- else
51
- break
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
- unless regexp =~ buffer.content
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
- enum_for(:scan)
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
- issue_reported = true
36
- reporter.issue(issue)
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 !rule.passes.empty? || !rule.fails.empty?
59
+ if rule.triggers.any? {|trigger| !trigger.passes.empty? || !trigger.fails.empty?}
60
60
  stdout.puts "Testing rule #{rule.id}..."
61
61
 
62
- pass_errors = rule.passes.each.with_index.select do |pass, index|
63
- rule_matches_example?(rule, pass)
64
- end
65
-
66
- fail_errors = rule.fails.each.with_index.reject do |fail, index|
67
- rule_matches_example?(rule, fail)
68
- end
69
-
70
- unless pass_errors.empty?
71
- test_pass = false
72
-
73
- pass_errors.each do |_, index|
74
- stdout.puts " #{(index+1).ordinalize} pass example matched.😱"
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
- unless fail_errors.empty?
79
- test_pass = false
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 pass_errors.empty? && fail_errors.empty?
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
@@ -24,20 +24,22 @@ module Goodcheck
24
24
  end
25
25
  end
26
26
 
27
- def rules_for_path(path, rules_filter:, &block)
27
+ def rules_for_path(path, rules_filter:)
28
28
  if block_given?
29
29
  each_rule(filter: rules_filter).map do |rule|
30
- globs = rule.patterns.flat_map(&:globs).push(*rule.globs)
30
+ rule.triggers.each do |trigger|
31
+ globs = trigger.globs
31
32
 
32
- if globs.empty?
33
- [rule, nil]
34
- else
35
- glob = globs.find {|glob| glob.test(path) }
36
- if glob
37
- [rule, glob]
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.compact.each(&block)
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(token: string, case_sensitive: boolean?, glob: optional(glob))
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
- patterns, negated = retrieve_patterns(hash)
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
- Rule.new(id: id, patterns: patterns, justifications: justifications, globs: globs, message: message, passes: passes, fails: fails, negated: negated)
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
- Pattern.literal(pattern, case_sensitive: true)
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
- globs = load_globs(array(pattern[:glob]))
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.literal(literal, case_sensitive: cs, globs: globs)
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.regexp(regexp, case_sensitive: cs, multiline: multiline, globs: globs)
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.token(tok, case_sensitive: cs, globs: globs)
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
@@ -11,5 +11,11 @@ module Goodcheck
11
11
  def test(path)
12
12
  path.fnmatch?(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB)
13
13
  end
14
+
15
+ def ==(other)
16
+ other.is_a?(Glob) &&
17
+ other.pattern == pattern &&
18
+ other.encoding == encoding
19
+ end
14
20
  end
15
21
  end
@@ -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
@@ -1,74 +1,215 @@
1
1
  module Goodcheck
2
- class Pattern
3
- attr_reader :source
4
- attr_reader :regexp
5
- attr_reader :globs
6
-
7
- def initialize(source:, regexp:, globs:)
8
- @source = source
9
- @regexp = regexp
10
- @globs = globs
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
- def self.literal(literal, case_sensitive:, globs: [])
14
- new(
15
- source: literal,
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
- def self.regexp(regexp, case_sensitive:, multiline:, globs: [])
22
- options = 0
23
- options |= Regexp::IGNORECASE unless case_sensitive
24
- options |= Regexp::MULTILINE if multiline
17
+ class Regexp
18
+ attr_reader :source
19
+ attr_reader :case_sensitive
20
+ attr_reader :multiline
25
21
 
26
- new(
27
- source: regexp,
28
- regexp: Regexp.compile(regexp, options),
29
- globs: globs
30
- )
31
- end
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
- def self.token(tokens, case_sensitive:, globs: [])
34
- new(
35
- source: tokens,
36
- regexp: compile_tokens(tokens, case_sensitive: case_sensitive),
37
- globs: globs
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
- def self.compile_tokens(source, case_sensitive:)
42
- tokens = []
43
- s = StringScanner.new(source)
44
-
45
- until s.eos?
46
- case
47
- when s.scan(/\(|\)|\{|\}|\[|\]|\<|\>/)
48
- tokens << Regexp.escape(s.matched)
49
- when s.scan(/\s+/)
50
- tokens << '\s+'
51
- when s.scan(/\w+|[\p{Letter}&&\p{^ASCII}]+/)
52
- tokens << Regexp.escape(s.matched)
53
- when s.scan(%r{[!"#$%&'=\-^~¥\\|`@*:+;/?.,]+})
54
- tokens << Regexp.escape(s.matched.rstrip)
55
- when s.scan(/./)
56
- tokens << Regexp.escape(s.matched)
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
- if tokens.first =~ /\A\p{Letter}/
61
- tokens.first.prepend('\b')
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
- if tokens.last =~ /\p{Letter}\Z/
65
- tokens.last << '\b'
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
- options = Regexp::MULTILINE
69
- options |= Regexp::IGNORECASE unless case_sensitive
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
- Regexp.new(tokens.join('\s*').gsub(/\\s\*(\\s\+\\s\*)+/, '\s+'), options)
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
@@ -1,26 +1,15 @@
1
1
  module Goodcheck
2
2
  class Rule
3
3
  attr_reader :id
4
- attr_reader :patterns
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:, patterns:, message:, justifications:, globs:, fails:, passes:, negated:)
8
+ def initialize(id:, triggers:, message:, justifications:)
12
9
  @id = id
13
- @patterns = patterns
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
@@ -1,3 +1,3 @@
1
1
  module Goodcheck
2
- VERSION = "1.7.1"
2
+ VERSION = "2.1.0"
3
3
  end
data/lib/goodcheck.rb CHANGED
@@ -23,6 +23,7 @@ require "goodcheck/array_helper"
23
23
  require "goodcheck/analyzer"
24
24
  require "goodcheck/issue"
25
25
  require "goodcheck/rule"
26
+ require "goodcheck/trigger"
26
27
  require "goodcheck/pattern"
27
28
  require "goodcheck/config"
28
29
  require "goodcheck/config_loader"
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.7.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-05-28 00:00:00.000000000 Z
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.1
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.1
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