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