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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b5456868e454a547396bd4b60fbf2b0ac444c2b9
4
- data.tar.gz: 3a04207f49e5ea83fea0b56b6be17211bd9ab60b
3
+ metadata.gz: 691aa5292cedd241455746b5e4f7cd4e4f72f82c
4
+ data.tar.gz: 47b1c349087fdc731b4740f40c15901a2c1db080
5
5
  SHA512:
6
- metadata.gz: 9ff30c3f252e24793e5a00b244a7c67453b64e2e746cf508647abfc8a35e7a02a28e243cd4593ac4e7e76aef557dcf4af4f9ce4890b3a028e166d1ce2c8ee94c
7
- data.tar.gz: 5aad7656fe6e61c7ca70542b379f16166929ccc288149c8b2e0230029cef6e4ff4d3b1dfe28231eaa139b19decbc6cbbc7051ffd03550cbbefa09d53f5bda07d
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
@@ -4,8 +4,7 @@ require "rainbow"
4
4
  require "parser/current"
5
5
  require "set"
6
6
  require "open3"
7
-
8
- Parser::Builders::Default.emit_lambda = true
7
+ require "active_support/inflector"
9
8
 
10
9
  require "querly/version"
11
10
  require 'querly/analyzer'
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"
@@ -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
 
@@ -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],
@@ -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}th *before* example didn't match with any pattern")
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}th *before* example")
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}th *after* example matched with some of patterns")
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}th *after* example")
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
@@ -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 name == node.children[1]
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
@@ -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 = Array(hash["pattern"])
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(src)
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(src.split("\n").first).blue}: #{exn}"
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
- script = Script.new(path: path, node: Parser::CurrentRuby.parse(source, path.to_s))
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
@@ -1,3 +1,3 @@
1
1
  module Querly
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/querly.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency 'thor', "~> 0.19"
31
31
  spec.add_dependency "parser", "~> 2.4.0"
32
32
  spec.add_dependency "rainbow", "~> 2.1"
33
+ spec.add_dependency "activesupport", "~> 5.1.1"
33
34
  end
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.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-05-25 00:00:00.000000000 Z
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.