querly 0.4.0 → 0.5.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 +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.
|