querly 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/querly.rb +1 -2
- data/lib/querly/cli.rb +1 -1
- data/lib/querly/cli/console.rb +3 -2
- data/lib/querly/cli/formatter.rb +6 -0
- data/lib/querly/cli/test.rb +55 -7
- data/lib/querly/pattern/expr.rb +6 -2
- data/lib/querly/pattern/parser.y +13 -3
- data/lib/querly/rule.rb +54 -5
- data/lib/querly/script_enumerator.rb +20 -1
- data/lib/querly/version.rb +1 -1
- data/querly.gemspec +1 -0
- data/sample.yaml +20 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 691aa5292cedd241455746b5e4f7cd4e4f72f82c
|
4
|
+
data.tar.gz: 47b1c349087fdc731b4740f40c15901a2c1db080
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf05476ed45253cd0738db867ba33325bd00e3b4eee6bb20f2eb0caafd19a3a7672d31a2e3db8491e44e1a397f53314757249cc815fa67a0d4bc6d25d0223332
|
7
|
+
data.tar.gz: b99a104c7d4914d195226b04c06b87b6243a79634b35935fea05fa7dae3cfd563f63dc5cd99ef0a3dff3c87babb35b1d0efd700f59306c032f5013840c796b84
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,18 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.5.0 (2017-06-16)
|
6
|
+
|
7
|
+
* Exit 1 on test failure #9
|
8
|
+
* Fix example index printing in test (@pocke) #8, #10
|
9
|
+
* Introduce pattern matching on method name by set of string and regexp
|
10
|
+
* Rule definitions in config can have more structured `examples` attribute
|
11
|
+
|
12
|
+
## 0.4.0 (2017-05-25)
|
13
|
+
|
14
|
+
* Update `parser` to 2.4 compatible version
|
15
|
+
* Check more pathnames which looks like Ruby by default (@pocke) #7
|
16
|
+
|
5
17
|
## 0.3.1 (2017-02-16)
|
6
18
|
|
7
19
|
* Allow `require` rules from config file
|
data/lib/querly.rb
CHANGED
data/lib/querly/cli.rb
CHANGED
@@ -71,7 +71,7 @@ Specify configuration file by --config option.
|
|
71
71
|
option :config, default: "querly.yml"
|
72
72
|
def test()
|
73
73
|
require "querly/cli/test"
|
74
|
-
Test.new(config_path: config_path).run
|
74
|
+
exit Test.new(config_path: config_path).run
|
75
75
|
end
|
76
76
|
|
77
77
|
desc "rules", "Print loaded rules"
|
data/lib/querly/cli/console.rb
CHANGED
@@ -42,7 +42,8 @@ Querly #{VERSION}, interactive console
|
|
42
42
|
when Script
|
43
43
|
@analyzer.scripts << script
|
44
44
|
when StandardError
|
45
|
-
p script
|
45
|
+
p path: path, script: script.inspect
|
46
|
+
p script.backtrace
|
46
47
|
end
|
47
48
|
end
|
48
49
|
|
@@ -61,7 +62,7 @@ Querly #{VERSION}, interactive console
|
|
61
62
|
STDOUT.puts " done"
|
62
63
|
when /^find (.+)/
|
63
64
|
begin
|
64
|
-
pattern = Pattern::Parser.parse($1)
|
65
|
+
pattern = Pattern::Parser.parse($1, where: {})
|
65
66
|
|
66
67
|
count = 0
|
67
68
|
|
data/lib/querly/cli/formatter.rb
CHANGED
@@ -122,6 +122,12 @@ module Querly
|
|
122
122
|
id: rule.id,
|
123
123
|
messages: rule.messages,
|
124
124
|
justifications: rule.justifications,
|
125
|
+
examples: rule.examples.map {|example|
|
126
|
+
{
|
127
|
+
before: example.before,
|
128
|
+
after: example.after
|
129
|
+
}
|
130
|
+
}
|
125
131
|
},
|
126
132
|
location: {
|
127
133
|
start: [pair.node.loc.first_line, pair.node.loc.column],
|
data/lib/querly/cli/test.rb
CHANGED
@@ -9,6 +9,15 @@ module Querly
|
|
9
9
|
@config_path = config_path
|
10
10
|
@stdout = stdout
|
11
11
|
@stderr = stderr
|
12
|
+
@success = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def fail!
|
16
|
+
@success = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def failed?
|
20
|
+
!@success
|
12
21
|
end
|
13
22
|
|
14
23
|
def run
|
@@ -17,15 +26,19 @@ module Querly
|
|
17
26
|
unless config
|
18
27
|
stdout.puts "There is nothing to test at #{config_path} ..."
|
19
28
|
stdout.puts "Make a configuration and run test again!"
|
20
|
-
return
|
29
|
+
return 1
|
21
30
|
end
|
22
31
|
|
23
32
|
validate_rule_uniqueness(config.rules)
|
24
33
|
validate_rule_patterns(config.rules)
|
34
|
+
|
35
|
+
failed? ? 1 : 0
|
25
36
|
rescue => exn
|
26
37
|
stderr.puts Rainbow("Fatal error:").red
|
27
38
|
stderr.puts exn.inspect
|
28
39
|
stderr.puts exn.backtrace.map {|x| " " + x }.join("\n")
|
40
|
+
|
41
|
+
1
|
29
42
|
end
|
30
43
|
|
31
44
|
def validate_rule_uniqueness(rules)
|
@@ -41,6 +54,8 @@ module Querly
|
|
41
54
|
duplications += 1
|
42
55
|
end
|
43
56
|
end
|
57
|
+
|
58
|
+
fail! unless duplications == 0
|
44
59
|
end
|
45
60
|
|
46
61
|
def validate_rule_patterns(rules)
|
@@ -52,31 +67,59 @@ module Querly
|
|
52
67
|
errors = 0
|
53
68
|
|
54
69
|
rules.each do |rule|
|
55
|
-
rule.before_examples.each.with_index do |example, example_index|
|
70
|
+
rule.before_examples.each.with_index(1) do |example, example_index|
|
56
71
|
tests += 1
|
57
72
|
|
58
73
|
begin
|
59
74
|
unless rule.patterns.any? {|pat| test_pattern(pat, example, expected: true) }
|
60
|
-
stdout.puts(Rainbow(" #{rule.id}").red + ":\t#{example_index}
|
75
|
+
stdout.puts(Rainbow(" #{rule.id}").red + ":\t#{ordinalize example_index} *before* example didn't match with any pattern")
|
61
76
|
false_negatives += 1
|
62
77
|
end
|
63
78
|
rescue Parser::SyntaxError
|
64
79
|
errors += 1
|
65
|
-
stdout.puts(Rainbow(" #{rule.id}").red + ":\tParsing failed for #{example_index}
|
80
|
+
stdout.puts(Rainbow(" #{rule.id}").red + ":\tParsing failed for #{ordinalize example_index} *before* example")
|
66
81
|
end
|
67
82
|
end
|
68
83
|
|
69
|
-
rule.after_examples.each.with_index do |example, example_index|
|
84
|
+
rule.after_examples.each.with_index(1) do |example, example_index|
|
70
85
|
tests += 1
|
71
86
|
|
72
87
|
begin
|
73
88
|
unless rule.patterns.all? {|pat| test_pattern(pat, example, expected: false) }
|
74
|
-
stdout.puts(Rainbow(" #{rule.id}").red + ":\t#{example_index}
|
89
|
+
stdout.puts(Rainbow(" #{rule.id}").red + ":\t#{ordinalize example_index} *after* example matched with some of patterns")
|
75
90
|
false_positives += 1
|
76
91
|
end
|
77
92
|
rescue Parser::SyntaxError
|
78
93
|
errors += 1
|
79
|
-
stdout.puts(Rainbow(" #{rule.id}") + ":\tParsing failed for #{example_index}
|
94
|
+
stdout.puts(Rainbow(" #{rule.id}") + ":\tParsing failed for #{ordinalize example_index} *after* example")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
rule.examples.each.with_index(1) do |example, index|
|
99
|
+
if example.before
|
100
|
+
tests += 1
|
101
|
+
begin
|
102
|
+
unless rule.patterns.any? {|pat| test_pattern(pat, example.before, expected: true) }
|
103
|
+
stdout.puts(Rainbow(" #{rule.id}").red + ":\tbefore of #{ordinalize index} example didn't match with any pattern")
|
104
|
+
false_negatives += 1
|
105
|
+
end
|
106
|
+
rescue Parser::SyntaxError
|
107
|
+
errors += 1
|
108
|
+
stdout.puts(Rainbow(" #{rule.id}").red + ":\tParsing failed on before of #{ordinalize index} example")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
if example.after
|
113
|
+
tests += 1
|
114
|
+
begin
|
115
|
+
unless rule.patterns.all? {|pat| test_pattern(pat, example.after, expected: false) }
|
116
|
+
stdout.puts(Rainbow(" #{rule.id}").red + ":\tafter of #{ordinalize index} example matched with some of patterns")
|
117
|
+
false_positives += 1
|
118
|
+
end
|
119
|
+
rescue Parser::SyntaxError
|
120
|
+
errors += 1
|
121
|
+
stdout.puts(Rainbow(" #{rule.id}") + ":\tParsing failed on after of #{ordinalize index} example")
|
122
|
+
end
|
80
123
|
end
|
81
124
|
end
|
82
125
|
end
|
@@ -86,6 +129,7 @@ module Querly
|
|
86
129
|
stdout.puts " #{false_positives} examples found which should not match, but matched"
|
87
130
|
stdout.puts " #{false_negatives} examples found which should match, but didn't"
|
88
131
|
stdout.puts " #{errors} examples raised error"
|
132
|
+
fail!
|
89
133
|
else
|
90
134
|
stdout.puts Rainbow(" All tests green!").green
|
91
135
|
end
|
@@ -112,6 +156,10 @@ module Querly
|
|
112
156
|
Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath, stderr: STDERR)
|
113
157
|
end
|
114
158
|
end
|
159
|
+
|
160
|
+
def ordinalize(number)
|
161
|
+
ActiveSupport::Inflector.ordinalize(number)
|
162
|
+
end
|
115
163
|
end
|
116
164
|
end
|
117
165
|
end
|
data/lib/querly/pattern/expr.rb
CHANGED
@@ -142,7 +142,7 @@ module Querly
|
|
142
142
|
attr_reader :block
|
143
143
|
|
144
144
|
def initialize(receiver:, name:, block:, args: Argument::AnySeq.new)
|
145
|
-
@name = name
|
145
|
+
@name = Array(name)
|
146
146
|
@receiver = receiver
|
147
147
|
@args = args
|
148
148
|
@block = block
|
@@ -161,6 +161,10 @@ module Querly
|
|
161
161
|
test_node pair.node
|
162
162
|
end
|
163
163
|
|
164
|
+
def test_name(node)
|
165
|
+
name.any? {|n| n === node.children[1] }
|
166
|
+
end
|
167
|
+
|
164
168
|
def test_node(node)
|
165
169
|
return false if block == true && node.type != :block
|
166
170
|
return false if block == false && node.type == :block
|
@@ -169,7 +173,7 @@ module Querly
|
|
169
173
|
|
170
174
|
case node&.type
|
171
175
|
when :send
|
172
|
-
return false unless
|
176
|
+
return false unless test_name(node)
|
173
177
|
return false unless test_receiver(node.children[0])
|
174
178
|
return false unless test_args(node.children.drop(2), args)
|
175
179
|
true
|
data/lib/querly/pattern/parser.y
CHANGED
@@ -55,6 +55,7 @@ key_value: keyword COLON expr { result = { key: val[0], value: val[2], negated:
|
|
55
55
|
|
56
56
|
method_name: METHOD
|
57
57
|
| EXCLAMATION
|
58
|
+
| META { result = resolve_meta(val[0]) }
|
58
59
|
|
59
60
|
method_name_or_ident: method_name
|
60
61
|
| LIDENT
|
@@ -103,14 +104,16 @@ end
|
|
103
104
|
require "strscan"
|
104
105
|
|
105
106
|
attr_reader :input
|
107
|
+
attr_reader :where
|
106
108
|
|
107
|
-
def initialize(input)
|
109
|
+
def initialize(input, where:)
|
108
110
|
super()
|
109
111
|
@input = StringScanner.new(input)
|
112
|
+
@where = where
|
110
113
|
end
|
111
114
|
|
112
|
-
def self.parse(str)
|
113
|
-
self.new(str).do_parse
|
115
|
+
def self.parse(str, where:)
|
116
|
+
self.new(str, where: where).do_parse
|
114
117
|
end
|
115
118
|
|
116
119
|
def next_token
|
@@ -158,6 +161,9 @@ def next_token
|
|
158
161
|
[:UIDENT, input.matched.to_sym]
|
159
162
|
when input.scan(/self/)
|
160
163
|
[:SELF, nil]
|
164
|
+
when input.scan(/'[a-z]\w*/)
|
165
|
+
s = input.matched
|
166
|
+
[:META, s[1, s.size - 1].to_sym]
|
161
167
|
when input.scan(/[a-z_](\w)*(\?|\!|=)?/)
|
162
168
|
[:LIDENT, input.matched.to_sym]
|
163
169
|
when input.scan(/\(/)
|
@@ -198,3 +204,7 @@ def next_token
|
|
198
204
|
[:AMP, nil]
|
199
205
|
end
|
200
206
|
end
|
207
|
+
|
208
|
+
def resolve_meta(name)
|
209
|
+
where[name] or raise Racc::ParseError, "Undefined meta variable: '#{name}"
|
210
|
+
end
|
data/lib/querly/rule.rb
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
module Querly
|
2
2
|
class Rule
|
3
|
+
class Example
|
4
|
+
attr_reader :before
|
5
|
+
attr_reader :after
|
6
|
+
|
7
|
+
def initialize(before:, after:)
|
8
|
+
@before = before
|
9
|
+
@after = after
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
other.is_a?(Example) && other.before == before && other.after == after
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
3
17
|
attr_reader :id
|
4
18
|
attr_reader :patterns
|
5
19
|
attr_reader :messages
|
@@ -8,9 +22,10 @@ module Querly
|
|
8
22
|
attr_reader :justifications
|
9
23
|
attr_reader :before_examples
|
10
24
|
attr_reader :after_examples
|
25
|
+
attr_reader :examples
|
11
26
|
attr_reader :tags
|
12
27
|
|
13
|
-
def initialize(id:, messages:, patterns:, sources:, tags:, before_examples:, after_examples:, justifications:)
|
28
|
+
def initialize(id:, messages:, patterns:, sources:, tags:, before_examples:, after_examples:, justifications:, examples:)
|
14
29
|
@id = id
|
15
30
|
@patterns = patterns
|
16
31
|
@sources = sources
|
@@ -19,6 +34,7 @@ module Querly
|
|
19
34
|
@before_examples = before_examples
|
20
35
|
@after_examples = after_examples
|
21
36
|
@tags = tags
|
37
|
+
@examples = examples
|
22
38
|
end
|
23
39
|
|
24
40
|
def match?(identifier: nil, tags: nil)
|
@@ -44,13 +60,30 @@ module Querly
|
|
44
60
|
id = hash["id"]
|
45
61
|
raise InvalidRuleHashError, "id is missing" unless id
|
46
62
|
|
47
|
-
srcs =
|
63
|
+
srcs = case hash["pattern"]
|
64
|
+
when Array
|
65
|
+
hash["pattern"]
|
66
|
+
when nil
|
67
|
+
[]
|
68
|
+
else
|
69
|
+
[hash["pattern"]]
|
70
|
+
end
|
71
|
+
|
48
72
|
raise InvalidRuleHashError, "pattern is missing" if srcs.empty?
|
49
73
|
patterns = srcs.map.with_index do |src, index|
|
74
|
+
case src
|
75
|
+
when String
|
76
|
+
subject = src
|
77
|
+
where = {}
|
78
|
+
when Hash
|
79
|
+
subject = src['subject']
|
80
|
+
where = Hash[src['where'].map {|k,v| [k.to_sym, translate_where(v)] }]
|
81
|
+
end
|
82
|
+
|
50
83
|
begin
|
51
|
-
Pattern::Parser.parse(
|
84
|
+
Pattern::Parser.parse(subject, where: where)
|
52
85
|
rescue Racc::ParseError => exn
|
53
|
-
raise PatternSyntaxError, "Pattern syntax error: rule=#{hash["id"]}, index=#{index}, pattern=#{Rainbow(
|
86
|
+
raise PatternSyntaxError, "Pattern syntax error: rule=#{hash["id"]}, index=#{index}, pattern=#{Rainbow(subject.split("\n").first).blue}, where=#{where.inspect}: #{exn}"
|
54
87
|
end
|
55
88
|
end
|
56
89
|
|
@@ -58,6 +91,10 @@ module Querly
|
|
58
91
|
raise InvalidRuleHashError, "message is missing" if messages.empty?
|
59
92
|
|
60
93
|
tags = Set.new(Array(hash["tags"]))
|
94
|
+
examples = [hash["examples"]].compact.flatten.map do |example|
|
95
|
+
raise(InvalidRuleHashError, "Example should have at least before or after, #{example.inspect}") unless example.key?("before") || example.key?("after")
|
96
|
+
Example.new(before: example["before"], after: example["after"])
|
97
|
+
end
|
61
98
|
before_examples = Array(hash["before"])
|
62
99
|
after_examples = Array(hash["after"])
|
63
100
|
justifications = Array(hash["justification"])
|
@@ -69,7 +106,19 @@ module Querly
|
|
69
106
|
tags: tags,
|
70
107
|
before_examples: before_examples,
|
71
108
|
after_examples: after_examples,
|
72
|
-
justifications: justifications
|
109
|
+
justifications: justifications,
|
110
|
+
examples: examples)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.translate_where(value)
|
114
|
+
Array(value).map do |v|
|
115
|
+
case v
|
116
|
+
when /\A\/(.*)\/\Z/
|
117
|
+
Regexp.new($1)
|
118
|
+
else
|
119
|
+
v.to_sym
|
120
|
+
end
|
121
|
+
end
|
73
122
|
end
|
74
123
|
end
|
75
124
|
end
|
@@ -46,7 +46,9 @@ module Querly
|
|
46
46
|
path.read
|
47
47
|
end
|
48
48
|
|
49
|
-
|
49
|
+
buffer = Parser::Source::Buffer.new(path.to_s, 1)
|
50
|
+
buffer.source = source
|
51
|
+
script = Script.new(path: path, node: parser.parse(buffer))
|
50
52
|
rescue StandardError, LoadError, Preprocessor::Error => exn
|
51
53
|
script = exn
|
52
54
|
end
|
@@ -54,6 +56,13 @@ module Querly
|
|
54
56
|
yield(path, script)
|
55
57
|
end
|
56
58
|
|
59
|
+
def parser
|
60
|
+
Parser::CurrentRuby.new(Builder.new).tap do |parser|
|
61
|
+
parser.diagnostics.all_errors_are_fatal = true
|
62
|
+
parser.diagnostics.ignore_warnings = true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
57
66
|
def preprocessors
|
58
67
|
config&.preprocessors || {}
|
59
68
|
end
|
@@ -93,5 +102,15 @@ module Querly
|
|
93
102
|
load_script_from_path(path, &block) if should_load_file
|
94
103
|
end
|
95
104
|
end
|
105
|
+
|
106
|
+
class Builder < Parser::Builders::Default
|
107
|
+
def string_value(token)
|
108
|
+
value(token)
|
109
|
+
end
|
110
|
+
|
111
|
+
def emit_lambda
|
112
|
+
true
|
113
|
+
end
|
114
|
+
end
|
96
115
|
end
|
97
116
|
end
|
data/lib/querly/version.rb
CHANGED
data/querly.gemspec
CHANGED
data/sample.yaml
CHANGED
@@ -125,6 +125,26 @@ rules:
|
|
125
125
|
- records.order.where.group
|
126
126
|
message: |
|
127
127
|
Using both group and order may generate broken SQL
|
128
|
+
- id: sample.pp_meta
|
129
|
+
message: |
|
130
|
+
Method names can be a meta variable reference
|
131
|
+
|
132
|
+
Meta variable starts with single quote ', and followed with lower letter.
|
133
|
+
pattern:
|
134
|
+
- subject: "'p(...)"
|
135
|
+
where:
|
136
|
+
p: /p+/
|
137
|
+
- id: sample.count
|
138
|
+
pattern: count() !{}
|
139
|
+
message: |
|
140
|
+
Use size or length for count, if receiver is an array
|
141
|
+
examples:
|
142
|
+
- before: "[].count"
|
143
|
+
after: "[].size"
|
144
|
+
- after: "[].count(:x)"
|
145
|
+
- after: "[].count {|x| x > 3 }"
|
146
|
+
- before: "[].count(x)"
|
147
|
+
after: "[].count"
|
128
148
|
|
129
149
|
preprocessor:
|
130
150
|
.slim: slimrb --compile
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: querly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.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: 2017-
|
11
|
+
date: 2017-06-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '2.1'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: activesupport
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 5.1.1
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 5.1.1
|
125
139
|
description: Querly is a query language and tool to find out method calls from Ruby
|
126
140
|
programs. Define rules to check your program with patterns to find out *bad* pieces.
|
127
141
|
Querly finds out matching pieces from your program.
|