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