querly 0.1.0 → 0.2.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: b38a9eb384827611a39b2d1bef7e1b8cb574f2c8
4
- data.tar.gz: c02c0c2ebb9844465855e2691e02715b5c73b359
3
+ metadata.gz: 23d5bf2430a932df4208453cc4847d06915a5140
4
+ data.tar.gz: 6be0f56ed32a3bded3ef3b9241633faedfc83c3a
5
5
  SHA512:
6
- metadata.gz: e9a5550cec1e87aad9dcfd0372af5a65ec82b46f685c8960c89b7a34316e64ffc2172136a4a238dfa3803349796df235d960ca4ca24df503f8895c4776a9631f
7
- data.tar.gz: fa20ac97b8485b262e625e20a945719c254a7a13f4006e0e8513156567324e3b932c6d75e623b16062085026eb9ad8e4ad902adb169f786026588ebd64cfc729
6
+ metadata.gz: 68bfb69f1c7628a24f2738bf5be828ce7b31d181a82f550ad1ed54b451d7612fa2ff7f4e6a6b6bf229dbfe147c13ff2a0656436c6b2680cfc800bd70106a34c3
7
+ data.tar.gz: 9794d372352bd5cff6704b93604ea6b94942a6e4eaf882602788b0660c579e80bfae6369843b5eb75cb55ae3d97848123e52517b4acbb825d1f961e731acacc0
@@ -0,0 +1,15 @@
1
+ # Change Log
2
+
3
+ ## 0.2.0 (2016-11-24)
4
+
5
+ * Remove `tagging` section from config
6
+ * Add `check` section to select rules to check
7
+ * Add `import` section to load rules from other file
8
+ * Add `querly rules` sub command to print loaded rules
9
+ * Add *with block* and *without block* pattern (`foo {}` / `foo !{}`)
10
+ * Add *some of receiver chain* pattern (`...`)
11
+ * Fix keyword args pattern matching bug
12
+
13
+ ## 0.1.0
14
+
15
+ * First release.
data/README.md CHANGED
@@ -1,22 +1,23 @@
1
- # Querly - Query Method Calls from Ruby Programs
1
+ # Querly - Pattern Based Checking Tool for Ruby
2
2
 
3
3
  [![Build Status](https://travis-ci.org/soutaro/querly.svg?branch=master)](https://travis-ci.org/soutaro/querly)
4
4
 
5
5
  Querly is a query language and tool to find out method calls from Ruby programs.
6
- You write simple query for method calls, and Querly finds out *wrong* pieces from your program.
6
+ Define rules to check your program with patterns to find out *bad* pieces.
7
+ Querly finds out matching pieces from your program.
7
8
 
8
9
  ## Overview
9
10
 
10
- Your project should have many local rules:
11
+ Your project may have many local rules:
11
12
 
12
- * Should not use `Customer#update_mail` but 30x faster `Customer.update_all_email` (Slower `#update_mail` is left just for existing code, but new code should not use it)
13
+ * Should not use `Customer#update_mail` and use 30x faster `Customer.update_all_email` instead (Slower `#update_mail` is left just for existing code, but new code should not use it)
13
14
  * Should not use `root_url` without `locale:` parameter
14
15
  * Should not use `Net::HTTP` for Web API calls, but use `HTTPClient`
15
16
 
16
17
  These local rule violations will be found during code review.
17
- Reviewers will ask commiter to revise, commiter will fix, fine.
18
+ Reviewers will ask commiter to revise; commiter will fix; fine.
18
19
  Really?
19
- It is boring and time consuming.
20
+ It is boring and time-consuming.
20
21
  We need some automation!
21
22
 
22
23
  However, that rules cannot be the standard.
@@ -43,7 +44,7 @@ rules:
43
44
  message: Use HTTPClient to make HTTP request
44
45
  ```
45
46
 
46
- Write down your local rules, and let Querly do boring checks.
47
+ Write down your local rules, and let Querly check conformance with them.
47
48
  Focus on spec, design, UX, and other important things during code review!
48
49
 
49
50
  ## Installation
@@ -66,8 +67,8 @@ Copy the following YAML and paste as `querly.yml` in your project's repo.
66
67
  rules:
67
68
  - id: sample.debug_print
68
69
  pattern:
69
- - p
70
- - pp
70
+ - self.p
71
+ - self.pp
71
72
  message: Delete debug print
72
73
  ```
73
74
 
@@ -120,7 +121,7 @@ There will be many false positives, and *querly warning free code* does not make
120
121
 
121
122
  * TODO: support to ignore warnings through magic comments in code
122
123
 
123
- This is not to ensure *there is nothing wrong in the code*, but just tells you *code fragments you should review with special care*.
124
+ Querly is not to ensure *there is nothing wrong in the code*, but just tells you *code fragments you should review with special care*.
124
125
  I believe it still improves your software development productivity.
125
126
 
126
127
  ### Incoming updates?
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(__dir__, "../lib")
4
+
5
+ require "querly/pp/cli"
6
+
7
+ Querly::PP::CLI.new(ARGV).run
@@ -18,8 +18,8 @@ require 'querly/node_pair'
18
18
  require "querly/pattern/parser"
19
19
  require 'querly/pattern/kind'
20
20
  require "querly/config"
21
- require "querly/tagging"
22
21
  require "querly/preprocessor"
22
+ require "querly/check"
23
23
 
24
24
  module Querly
25
25
  # Your code goes here...
@@ -1,13 +1,11 @@
1
1
  module Querly
2
2
  class Analyzer
3
- attr_reader :rules
3
+ attr_reader :config
4
4
  attr_reader :scripts
5
- attr_reader :taggings
6
5
 
7
- def initialize(taggings:)
8
- @rules = []
6
+ def initialize(config:)
7
+ @config = config
9
8
  @scripts = []
10
- @taggings = taggings
11
9
  end
12
10
 
13
11
  #
@@ -15,14 +13,11 @@ module Querly
15
13
  #
16
14
  def run
17
15
  scripts.each do |script|
18
- tagging = taggings.find {|tagging| tagging.applicable?(script) }
19
-
20
- each_subnode script.root_pair do |node_pair|
16
+ rules = config.rules_for_path(script.path)
17
+ script.root_pair.each_subpair do |node_pair|
21
18
  rules.each do |rule|
22
- if applicable_rule?(tagging, rule)
23
- if rule.patterns.any? {|pattern| test_pair(node_pair, pattern) }
24
- yield script, rule, node_pair
25
- end
19
+ if rule.patterns.any? {|pattern| test_pair(node_pair, pattern) }
20
+ yield script, rule, node_pair
26
21
  end
27
22
  end
28
23
  end
@@ -31,7 +26,7 @@ module Querly
31
26
 
32
27
  def find(pattern)
33
28
  scripts.each do |script|
34
- each_subnode script.root_pair do |node_pair|
29
+ script.root_pair.each_subpair do |node_pair|
35
30
  if test_pair(node_pair, pattern)
36
31
  yield script, node_pair
37
32
  end
@@ -42,23 +37,5 @@ module Querly
42
37
  def test_pair(node_pair, pattern)
43
38
  pattern.expr =~ node_pair && pattern.test_kind(node_pair)
44
39
  end
45
-
46
- def applicable_rule?(tagging, rule)
47
- if tagging && !rule.tags.empty?
48
- tagging.tags_set.any? {|set| set.subset?(rule.tags) }
49
- else
50
- true
51
- end
52
- end
53
-
54
- def each_subnode(node_pair, &block)
55
- return unless node_pair.node
56
-
57
- yield node_pair
58
-
59
- node_pair.children.each do |child|
60
- each_subnode child, &block
61
- end
62
- end
63
40
  end
64
41
  end
@@ -0,0 +1,104 @@
1
+ module Querly
2
+ class Check
3
+ Query = Struct.new(:opr, :tags, :identifier) do
4
+ def apply(current:, all:)
5
+ case opr
6
+ when :append
7
+ current.union(all.select {|rule| match?(rule) })
8
+ when :except
9
+ current.reject {|rule| match?(rule) }.to_set
10
+ when :only
11
+ all.select {|rule| match?(rule) }.to_set
12
+ end
13
+ end
14
+
15
+ def match?(rule)
16
+ rule.match?(identifier: identifier, tags: tags)
17
+ end
18
+ end
19
+
20
+ attr_reader :patterns
21
+ attr_reader :rules
22
+
23
+ def initialize(pattern:, rules:)
24
+ @rules = rules
25
+
26
+ @has_trailing_slash = pattern.end_with?("/")
27
+ @has_middle_slash = /\/./ =~ pattern
28
+
29
+ @patterns = []
30
+
31
+ pattern.sub!(/\A\//, '')
32
+
33
+ case
34
+ when has_trailing_slash? && has_middle_slash?
35
+ patterns << File.join(pattern, "**")
36
+ when has_trailing_slash?
37
+ patterns << File.join(pattern, "**")
38
+ patterns << File.join("**", pattern, "**")
39
+ when has_middle_slash?
40
+ patterns << pattern
41
+ patterns << File.join(pattern, "**")
42
+ else
43
+ patterns << pattern
44
+ patterns << File.join("**", pattern)
45
+ patterns << File.join(pattern, "**")
46
+ patterns << File.join("**", pattern, "**")
47
+ end
48
+ end
49
+
50
+ def has_trailing_slash?
51
+ @has_trailing_slash
52
+ end
53
+
54
+ def has_middle_slash?
55
+ @has_middle_slash
56
+ end
57
+
58
+ def self.load(hash)
59
+ pattern = hash["path"]
60
+
61
+ rules = Array(hash["rules"]).map do |rule|
62
+ case rule
63
+ when String
64
+ parse_rule_query(:append, rule)
65
+ when Hash
66
+ case
67
+ when rule["append"]
68
+ parse_rule_query(:append, rule["append"])
69
+ when rule["except"]
70
+ parse_rule_query(:except, rule["except"])
71
+ when rule["only"]
72
+ parse_rule_query(:only, rule["only"])
73
+ else
74
+ parse_rule_query(:append, rule)
75
+ end
76
+ end
77
+ end
78
+
79
+ self.new(pattern: pattern, rules: rules)
80
+ end
81
+
82
+ def self.parse_rule_query(opr, query)
83
+ case query
84
+ when String
85
+ Query.new(opr, nil, query)
86
+ when Hash
87
+ if query['tags']
88
+ ts = query['tags']
89
+ if ts.is_a?(String)
90
+ ts = ts.split
91
+ end
92
+ tags = Set.new(ts)
93
+ end
94
+ identifier = query['id']
95
+
96
+ Query.new(opr, tags, identifier)
97
+ end
98
+ end
99
+
100
+ def match?(path:)
101
+ patterns.any? {|pat| File.fnmatch?(pat, path.to_s) }
102
+ end
103
+ end
104
+ end
@@ -5,6 +5,7 @@ module Querly
5
5
  class CLI < Thor
6
6
  desc "check [paths]", "Check paths based on configuration"
7
7
  option :config, default: "querly.yml"
8
+ option :root
8
9
  option :format, default: "text", type: :string, enum: %w(text json)
9
10
  def check(*paths)
10
11
  require 'querly/cli/formatter'
@@ -26,18 +27,20 @@ Specify configuration file by --config option.
26
27
  exit 1
27
28
  end
28
29
 
29
- config = Config.new
30
- begin
31
- config.add_file config_path
30
+ root_option = options[:root]
31
+ root_path = root_option ? Pathname(root_option).realpath : config_path.parent.realpath
32
+
33
+ config = begin
34
+ yaml = YAML.load(config_path.read)
35
+ Config.load(yaml, config_path: config_path, root_dir: root_path, stderr: STDERR)
32
36
  rescue => exn
33
37
  formatter.config_error config_path, exn
34
38
  exit
35
39
  end
36
40
 
37
- analyzer = Analyzer.new(taggings: config.taggings)
38
- analyzer.rules.concat config.rules
41
+ analyzer = Analyzer.new(config: config)
39
42
 
40
- ScriptEnumerator.new(paths: paths.map {|path| Pathname(path) }, preprocessors: config.preprocessors).each do |path, script|
43
+ ScriptEnumerator.new(paths: paths.map {|path| Pathname(path) }, config: config).each do |path, script|
41
44
  case script
42
45
  when Script
43
46
  analyzer.scripts << script
@@ -71,6 +74,13 @@ Specify configuration file by --config option.
71
74
  Test.new(config_path: config_path).run
72
75
  end
73
76
 
77
+ desc "rules", "Print loaded rules"
78
+ option :config, default: "querly.yml"
79
+ def rules(*ids)
80
+ require "querly/cli/rules"
81
+ Rules.new(config_path: config_path, ids: ids).run
82
+ end
83
+
74
84
  private
75
85
 
76
86
  def config_path
@@ -33,9 +33,9 @@ Querly #{VERSION}, interactive console
33
33
  def analyzer
34
34
  return @analyzer if @analyzer
35
35
 
36
- @analyzer = Analyzer.new(taggings: [])
36
+ @analyzer = Analyzer.new(config: nil)
37
37
 
38
- ScriptEnumerator.new(paths: paths).each do |path, script|
38
+ ScriptEnumerator.new(paths: paths, config: nil).each do |path, script|
39
39
  case script
40
40
  when Script
41
41
  @analyzer.scripts << script
@@ -0,0 +1,77 @@
1
+ module Querly
2
+ class CLI
3
+ class Rules
4
+ attr_reader :config_path
5
+ attr_reader :stdout
6
+ attr_reader :ids
7
+
8
+ def initialize(config_path:, ids:, stdout: STDOUT)
9
+ @config_path = config_path
10
+ @stdout = stdout
11
+ @ids = ids
12
+ end
13
+
14
+ def config
15
+ yaml = YAML.load(config_path.read)
16
+ @config ||= Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath)
17
+ end
18
+
19
+ def run
20
+ rules = config.rules.select {|rule| test_rule(rule) }
21
+ stdout.puts YAML.dump(rules.map {|rule| rule_to_yaml(rule) })
22
+ end
23
+
24
+ def test_rule(rule)
25
+ if ids.empty?
26
+ true
27
+ else
28
+ ids.any? {|id| rule.match?(identifier: id) }
29
+ end
30
+ end
31
+
32
+ def rule_to_yaml(rule)
33
+ { "id" => rule.id }.tap do |hash|
34
+ singleton rule.sources do |a|
35
+ hash["pattern"] = a
36
+ end
37
+
38
+ singleton rule.messages do |a|
39
+ hash["message"] = a
40
+ end
41
+
42
+ empty rule.tags do |a|
43
+ hash["tags"] = a
44
+ end
45
+
46
+ singleton rule.justifications do |a|
47
+ hash["justification"] = a
48
+ end
49
+
50
+ singleton rule.before_examples do |a|
51
+ hash["before"] = a
52
+ end
53
+
54
+ singleton rule.after_examples do |a|
55
+ hash["after"] = a
56
+ end
57
+ end
58
+ end
59
+
60
+ def empty(array)
61
+ unless array.empty?
62
+ yield array.to_a
63
+ end
64
+ end
65
+
66
+ def singleton(array)
67
+ empty(array) do
68
+ if array.length == 1
69
+ yield array.first
70
+ else
71
+ yield array.to_a
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -92,12 +92,12 @@ module Querly
92
92
  end
93
93
 
94
94
  def test_pattern(pattern, example, expected:)
95
- analyzer = Analyzer.new(taggings: [])
95
+ analyzer = Analyzer.new(config: nil)
96
96
 
97
97
  found = false
98
98
 
99
99
  node = Parser::CurrentRuby.parse(example)
100
- analyzer.each_subnode NodePair.new(node: node) do |pair|
100
+ NodePair.new(node: node).each_subpair do |pair|
101
101
  if analyzer.test_pair(pair, pattern)
102
102
  found = true
103
103
  end
@@ -108,9 +108,8 @@ module Querly
108
108
 
109
109
  def load_config
110
110
  if config_path.file?
111
- config = Config.new
112
- config.add_file config_path
113
- config
111
+ yaml = YAML.load(config_path.read)
112
+ Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath, stderr: STDERR)
114
113
  end
115
114
  end
116
115
  end
@@ -1,55 +1,87 @@
1
1
  module Querly
2
2
  class Config
3
3
  attr_reader :rules
4
- attr_reader :paths
5
- attr_reader :taggings
6
4
  attr_reader :preprocessors
5
+ attr_reader :root_dir
6
+ attr_reader :checks
7
+ attr_reader :rules_cache
7
8
 
8
- def initialize()
9
- @rules = []
10
- @paths = []
11
- @taggings = []
12
- @preprocessors = {}
9
+ def initialize(rules:, preprocessors:, root_dir:, checks:)
10
+ @rules = rules
11
+ @root_dir = root_dir
12
+ @preprocessors = preprocessors
13
+ @checks = checks
14
+ @rules_cache = {}
13
15
  end
14
16
 
15
- def add_file(path)
16
- paths << path
17
+ def self.load(hash, config_path:, root_dir:, stderr: STDERR)
18
+ Factory.new(hash, config_path: config_path, root_dir: root_dir, stderr: stderr).config
19
+ end
17
20
 
18
- content = YAML.load(path.read)
19
- load_rules(content)
20
- load_taggings(content)
21
- load_preprocessors(content["preprocessor"] || {})
21
+ def all_rules
22
+ @all_rules ||= Set.new(rules)
22
23
  end
23
24
 
24
- def load_rules(yaml)
25
- yaml["rules"].each do |hash|
26
- id = hash["id"]
27
- patterns = Array(hash["pattern"]).map {|src| Pattern::Parser.parse(src) }
28
- messages = Array(hash["message"])
29
- justifications = Array(hash["justification"])
30
-
31
- rule = Rule.new(id: id)
32
- rule.patterns.concat patterns
33
- rule.messages.concat messages
34
- rule.justifications.concat justifications
35
- Array(hash["tags"]).each {|tag| rule.tags << tag }
36
- rule.before_examples.concat Array(hash["before"])
37
- rule.after_examples.concat Array(hash["after"])
38
-
39
- rules << rule
40
- end
25
+ def relative_path_from_root(path)
26
+ path.absolute? ? path.relative_path_from(root_dir) : path.cleanpath
41
27
  end
42
28
 
43
- def load_taggings(yaml)
44
- @taggings = Array(yaml["tagging"]).map {|hash|
45
- Tagging.new(path_pattern: hash["path"],
46
- tags_set: Array(hash["tags"]).map {|string| Set.new(string.split) })
47
- }.sort_by {|tagging| -tagging.path_pattern.size }
29
+ def rules_for_path(path)
30
+ relative_path = relative_path_from_root(path)
31
+ matching_checks = checks.select {|check| check.match?(path: relative_path) }
32
+
33
+ if rules_cache.key?(matching_checks)
34
+ rules_cache[matching_checks]
35
+ else
36
+ matching_checks.flat_map(&:rules).inject(all_rules) do |rules, query|
37
+ query.apply(current: rules, all: all_rules)
38
+ end.tap do |rules|
39
+ rules_cache[matching_checks] = rules
40
+ end
41
+ end
48
42
  end
49
43
 
50
- def load_preprocessors(preprocessors)
51
- @preprocessors = preprocessors.each.with_object({}) do |(key, value), hash|
52
- hash[key] = Preprocessor.new(ext: key, command: value)
44
+ class Factory
45
+ attr_reader :yaml
46
+ attr_reader :root_dir
47
+ attr_reader :stderr
48
+ attr_reader :config_path
49
+
50
+ def initialize(yaml, config_path:, root_dir:, stderr: STDERR)
51
+ @yaml = yaml
52
+ @config_path = config_path
53
+ @root_dir = root_dir
54
+ @stderr = stderr
55
+ end
56
+
57
+ def config
58
+ if yaml["tagging"]
59
+ stderr.puts "tagging is deprecated and ignored"
60
+ end
61
+
62
+ rules = Array(yaml["rules"]).map {|hash| Rule.load(hash) }
63
+ preprocessors = (yaml["preprocessor"] || {}).each.with_object({}) do |(key, value), hash|
64
+ hash[key] = Preprocessor.new(ext: key, command: value)
65
+ end
66
+
67
+ imports = Array(yaml["import"])
68
+ imports.each do |import|
69
+ if import["load"]
70
+ load_pattern = Pathname(import["load"])
71
+ load_pattern = config_path.parent + load_pattern if load_pattern.relative?
72
+
73
+ Pathname.glob(load_pattern.to_s) do |path|
74
+ stderr.puts "Loading rules from #{path}..."
75
+ YAML.load(path.read).each do |hash|
76
+ rules << Rule.load(hash)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ checks = Array(yaml["check"]).map {|hash| Check.load(hash) }
83
+
84
+ Config.new(rules: rules, preprocessors: preprocessors, checks: checks, root_dir: root_dir)
53
85
  end
54
86
  end
55
87
  end
@@ -17,5 +17,19 @@ module Querly
17
17
  end
18
18
  end
19
19
  end
20
+
21
+ def each_subpair(&block)
22
+ if block_given?
23
+ return unless node
24
+
25
+ yield self
26
+
27
+ children.each do |child|
28
+ child.each_subpair &block
29
+ end
30
+ else
31
+ enum_for :each_subpair
32
+ end
33
+ end
20
34
  end
21
35
  end
@@ -128,6 +128,9 @@ module Querly
128
128
  true
129
129
  end
130
130
 
131
+ when :regexp
132
+ type == :regexp
133
+
131
134
  end
132
135
  end
133
136
  end
@@ -136,18 +139,20 @@ module Querly
136
139
  attr_reader :name
137
140
  attr_reader :receiver
138
141
  attr_reader :args
142
+ attr_reader :block
139
143
 
140
- def initialize(receiver:, name:, args: Argument::AnySeq.new)
144
+ def initialize(receiver:, name:, block:, args: Argument::AnySeq.new)
141
145
  @name = name
142
146
  @receiver = receiver
143
147
  @args = args
148
+ @block = block
144
149
  end
145
150
 
146
151
  def =~(pair)
147
152
  # Skip send node with block
148
153
  if pair.node.type == :send && pair.parent
149
154
  if pair.parent.node.type == :block
150
- if pair.parent.node.children.first == pair.node
155
+ if pair.parent.node.children.first.equal? pair.node
151
156
  return false
152
157
  end
153
158
  end
@@ -157,6 +162,9 @@ module Querly
157
162
  end
158
163
 
159
164
  def test_node(node)
165
+ return false if block == true && node.type != :block
166
+ return false if block == false && node.type == :block
167
+
160
168
  node = node.children.first if node&.type == :block
161
169
 
162
170
  case node&.type
@@ -173,15 +181,19 @@ module Querly
173
181
 
174
182
  case args
175
183
  when Argument::AnySeq
176
- if args.tail && first_node
177
- case
178
- when nodes.last.type == :kwsplat
179
- true
180
- when nodes.last.type == :hash && args.tail.is_a?(Argument::KeyValue)
181
- hash = hash_node_to_hash(nodes.last)
182
- test_hash_args(hash, args.tail)
184
+ if args.tail
185
+ if first_node
186
+ case
187
+ when nodes.last.type == :kwsplat
188
+ true
189
+ when nodes.last.type == :hash && args.tail.is_a?(Argument::KeyValue)
190
+ hash = hash_node_to_hash(nodes.last)
191
+ test_hash_args(hash, args.tail)
192
+ else
193
+ test_hash_args({}, args.tail)
194
+ end
183
195
  else
184
- true
196
+ test_hash_args({}, args.tail)
185
197
  end
186
198
  else
187
199
  true
@@ -243,6 +255,22 @@ module Querly
243
255
  end
244
256
  end
245
257
 
258
+ class ReceiverContext < Base
259
+ attr_reader :receiver
260
+
261
+ def initialize(receiver:)
262
+ @receiver = receiver
263
+ end
264
+
265
+ def test_node(node)
266
+ if receiver.test_node(node)
267
+ true
268
+ else
269
+ node&.type == :send && test_node(node.children[0])
270
+ end
271
+ end
272
+ end
273
+
246
274
  class Vcall < Base
247
275
  attr_reader :name
248
276
 
@@ -22,6 +22,7 @@ expr: constant { result = Expr::Constant.new(path: val[0]) }
22
22
  | FLOAT { result = Expr::Literal.new(type: :float, value: val[0]) }
23
23
  | SYMBOL { result = Expr::Literal.new(type: :symbol, value: val[0]) }
24
24
  | NUMBER { result = Expr::Literal.new(type: :number, value: val[0]) }
25
+ | REGEXP { result = Expr::Literal.new(type: :regexp, value: nil) }
25
26
  | DSTR { result = Expr::Dstr.new() }
26
27
  | UNDERBAR { result = Expr::Any.new }
27
28
  | NIL { result = Expr::Nil.new }
@@ -64,14 +65,34 @@ constant: UIDENT { result = [val[0]] }
64
65
  | UIDENT COLONCOLON constant { result = [val[0]] + val[2] }
65
66
 
66
67
  send: LIDENT { result = Expr::Vcall.new(name: val[0]) }
67
- | method_name { result = Expr::Send.new(receiver: Expr::Any.new, name: val[0]) }
68
- | method_name_or_ident LPAREN args RPAREN { result = Expr::Send.new(receiver: Expr::Any.new,
69
- name: val[0],
70
- args: val[2]) }
71
- | expr DOT method_name_or_ident { result = Expr::Send.new(receiver: val[0], name: val[2], args: Argument::AnySeq.new) }
72
- | expr DOT method_name_or_ident LPAREN args RPAREN { result = Expr::Send.new(receiver: val[0],
73
- name: val[2],
74
- args: val[4]) }
68
+ | method_name { result = Expr::Send.new(receiver: Expr::Any.new, name: val[0], block: nil) }
69
+ | method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: Expr::Any.new,
70
+ name: val[0],
71
+ args: val[2],
72
+ block: val[4]) }
73
+ | receiver method_name_or_ident block { result = Expr::Send.new(receiver: val[0],
74
+ name: val[1],
75
+ args: Argument::AnySeq.new,
76
+ block: val[2]) }
77
+ | receiver method_name_or_ident block { result = Expr::Send.new(receiver: val[0],
78
+ name: val[1],
79
+ args: Argument::AnySeq.new,
80
+ block: val[2]) }
81
+ | receiver method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],
82
+ name: val[1],
83
+ args: val[3],
84
+ block: val[5]) }
85
+ | receiver method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],
86
+ name: val[1],
87
+ args: val[3],
88
+ block: val[5]) }
89
+
90
+ receiver: expr DOT { result = val[0] }
91
+ | expr DOTDOTDOT { result = Expr::ReceiverContext.new(receiver: val[0]) }
92
+
93
+ block: { result = nil }
94
+ | WITH_BLOCK { result = true }
95
+ | WITHOUT_BLOCK { result = false }
75
96
 
76
97
  end
77
98
 
@@ -116,9 +137,15 @@ def next_token
116
137
  [:SYMBOL, nil]
117
138
  when input.scan(/:number:/)
118
139
  [:NUMBER, nil]
140
+ when input.scan(/:regexp:/)
141
+ [:REGEXP, nil]
119
142
  when input.scan(/:\w+/)
120
143
  s = input.matched
121
144
  [:SYMBOL, s[1, s.size - 1].to_sym]
145
+ when input.scan(/{}/)
146
+ [:WITH_BLOCK, nil]
147
+ when input.scan(/!{}/)
148
+ [:WITHOUT_BLOCK, nil]
122
149
  when input.scan(/[+-]?[0-9]+\.[0-9]/)
123
150
  [:FLOAT, input.matched.to_f]
124
151
  when input.scan(/[+-]?[0-9]+/)
@@ -0,0 +1,74 @@
1
+ require "optparse"
2
+
3
+ module Querly
4
+ module PP
5
+ class CLI
6
+ attr_reader :argv
7
+ attr_reader :command
8
+ attr_reader :load_paths
9
+ attr_reader :requires
10
+
11
+ attr_reader :stdin
12
+ attr_reader :stderr
13
+ attr_reader :stdout
14
+
15
+ def initialize(argv, stdin: STDIN, stdout: STDOUT, stderr: STDERR)
16
+ @argv = argv
17
+ @stdin = stdin
18
+ @stdout = stdout
19
+ @stderr = stderr
20
+
21
+ @load_paths = []
22
+ @requires = []
23
+
24
+ OptionParser.new do |opts|
25
+ opts.banner = "Usage: #{opts.program_name} pp-name [options]"
26
+ opts.on("-I dir") {|path| load_paths << path }
27
+ opts.on("-r lib") {|rq| requires << rq }
28
+ end.permute!(argv)
29
+
30
+ @command = argv.shift&.to_sym
31
+ end
32
+
33
+ def load_libs
34
+ load_paths.each do |path|
35
+ $LOAD_PATH << path
36
+ end
37
+
38
+ requires.each do |lib|
39
+ require lib
40
+ end
41
+
42
+ end
43
+
44
+ def run
45
+ available_commands = [:haml]
46
+
47
+ if available_commands.include?(command)
48
+ send :"run_#{command}"
49
+ else
50
+ stderr.puts "Unknown command: #{command}"
51
+ stderr.puts " available commands: #{available_commands.join(", ")}"
52
+ exit 1
53
+ end
54
+ end
55
+
56
+ def run_haml
57
+ require "haml"
58
+ load_libs
59
+
60
+ source = stdin.read
61
+
62
+ options = Haml::Options.new
63
+
64
+ parser = Haml::Parser.new(source, options)
65
+ parser.parse
66
+
67
+ compiler = Haml::Compiler.new(options)
68
+ compiler.compile(parser.root)
69
+
70
+ stdout.print compiler.precompiled
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,25 +2,67 @@ module Querly
2
2
  class Rule
3
3
  attr_reader :id
4
4
  attr_reader :patterns
5
-
6
5
  attr_reader :messages
6
+
7
+ attr_reader :sources
7
8
  attr_reader :justifications
8
9
  attr_reader :before_examples
9
10
  attr_reader :after_examples
10
11
  attr_reader :tags
11
- attr_reader :scope
12
12
 
13
- def initialize(id:, scope: :nil)
13
+ def initialize(id:, messages:, patterns:, sources:, tags:, before_examples:, after_examples:, justifications:)
14
14
  @id = id
15
- @scope = scope
16
-
17
- @patterns = []
18
- @messages = []
19
- @justifications = []
20
- @before_examples = []
21
- @after_examples = []
22
- @tags = Set.new
23
- @scope = scope
15
+ @patterns = patterns
16
+ @sources = sources
17
+ @messages = messages
18
+ @justifications = justifications
19
+ @before_examples = before_examples
20
+ @after_examples = after_examples
21
+ @tags = tags
22
+ end
23
+
24
+ def match?(identifier: nil, tags: nil)
25
+ if identifier
26
+ unless id == identifier || id.start_with?(identifier + ".")
27
+ return false
28
+ end
29
+ end
30
+
31
+ if tags
32
+ unless tags.subset?(self.tags)
33
+ return false
34
+ end
35
+ end
36
+
37
+ true
38
+ end
39
+
40
+ class InvalidRuleHashError < StandardError; end
41
+
42
+ def self.load(hash)
43
+ id = hash["id"]
44
+ raise InvalidRuleHashError, "id is missing" unless id
45
+
46
+ srcs = Array(hash["pattern"])
47
+ raise InvalidRuleHashError, "pattern is missing" if srcs.empty?
48
+ patterns = srcs.map {|src| Pattern::Parser.parse(src) }
49
+
50
+ messages = Array(hash["message"])
51
+ raise InvalidRuleHashError, "message is missing" if messages.empty?
52
+
53
+ tags = Set.new(Array(hash["tags"]))
54
+ before_examples = Array(hash["before"])
55
+ after_examples = Array(hash["after"])
56
+ justifications = Array(hash["justification"])
57
+
58
+ Rule.new(id: id,
59
+ messages: messages,
60
+ patterns: patterns,
61
+ sources: srcs,
62
+ tags: tags,
63
+ before_examples: before_examples,
64
+ after_examples: after_examples,
65
+ justifications: justifications)
24
66
  end
25
67
  end
26
68
  end
@@ -1,11 +1,11 @@
1
1
  module Querly
2
2
  class ScriptEnumerator
3
3
  attr_reader :paths
4
- attr_reader :preprocessors
4
+ attr_reader :config
5
5
 
6
- def initialize(paths:, preprocessors: {})
6
+ def initialize(paths:, config:)
7
7
  @paths = paths
8
- @preprocessors = preprocessors
8
+ @config = config
9
9
  end
10
10
 
11
11
  def each(&block)
@@ -54,6 +54,10 @@ module Querly
54
54
  yield(path, script)
55
55
  end
56
56
 
57
+ def preprocessors
58
+ config&.preprocessors || {}
59
+ end
60
+
57
61
  def enumerate_files_in_dir(path, &block)
58
62
  if path.basename.to_s =~ /\A\.[^\.]+/
59
63
  # skip hidden paths
@@ -1,3 +1,3 @@
1
1
  module Querly
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Soutaro Matsumoto"]
10
10
  spec.email = ["matsumoto@soutaro.com"]
11
11
 
12
- spec.summary = %q{Query Method Calls from Ruby Programs}
13
- spec.description = %q{Querly is a query language and tool to find out method calls from Ruby programs. You write simple query, and Querly finds out wrong pieces in your program.}
12
+ spec.summary = %q{Pattern Based Checking Tool for Ruby}
13
+ spec.description = %q{Querly is a query language and tool to find out method calls from Ruby programs. Define rules to check your program with patterns to find out *bad* pieces. Querly finds out matching pieces from your program.}
14
14
  spec.homepage = "https://github.com/soutaro/querly"
15
15
 
16
16
  spec.files = `git ls-files -z`
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency "rake", "~> 10.0"
26
26
  spec.add_development_dependency "minitest", "~> 5.0"
27
27
  spec.add_development_dependency "racc", "= 1.4.14"
28
+ spec.add_development_dependency "unification_assertion", "0.0.1"
28
29
 
29
30
  spec.add_dependency 'thor', "~> 0.19"
30
31
  spec.add_dependency "parser", "~> 2.3.1"
@@ -118,10 +118,34 @@ rules:
118
118
  tags:
119
119
  - test
120
120
  - capybara
121
+ - id: sample.order-group
122
+ pattern: order...group
123
+ before:
124
+ - records.where.order.group
125
+ - records.order.where.group
126
+ message: |
127
+ Using both group and order may generate broken SQL
128
+
121
129
  preprocessor:
122
130
  .slim: slimrb --compile
123
- tagging:
124
- - path: test
125
- tags:
126
- - path: features/step_definitions
127
- tags: test capybara
131
+
132
+ import:
133
+ - load: rules/*.yml
134
+
135
+ check:
136
+ - path: /
137
+ rules:
138
+ - except: minitest
139
+ - path: /test
140
+ rules:
141
+ - minitest
142
+ - except:
143
+ tags: capybara minitest
144
+ - path: /test/integration
145
+ rules:
146
+ - append:
147
+ tags: capybara minitest
148
+ - path: /features/step_definitions
149
+ rules:
150
+ - append:
151
+ tags: capybara minitest
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.1.0
4
+ version: 0.2.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: 2016-10-10 00:00:00.000000000 Z
11
+ date: 2016-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - '='
67
67
  - !ruby/object:Gem::Version
68
68
  version: 1.4.14
69
+ - !ruby/object:Gem::Dependency
70
+ name: unification_assertion
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.0.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.0.1
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: thor
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -109,27 +123,33 @@ dependencies:
109
123
  - !ruby/object:Gem::Version
110
124
  version: '2.1'
111
125
  description: Querly is a query language and tool to find out method calls from Ruby
112
- programs. You write simple query, and Querly finds out wrong pieces in your program.
126
+ programs. Define rules to check your program with patterns to find out *bad* pieces.
127
+ Querly finds out matching pieces from your program.
113
128
  email:
114
129
  - matsumoto@soutaro.com
115
130
  executables:
116
131
  - querly
132
+ - querly-pp
117
133
  extensions: []
118
134
  extra_rdoc_files: []
119
135
  files:
120
136
  - ".gitignore"
121
137
  - ".travis.yml"
138
+ - CHANGELOG.md
122
139
  - Gemfile
123
140
  - README.md
124
141
  - Rakefile
125
142
  - bin/console
126
143
  - bin/setup
127
144
  - exe/querly
145
+ - exe/querly-pp
128
146
  - lib/querly.rb
129
147
  - lib/querly/analyzer.rb
148
+ - lib/querly/check.rb
130
149
  - lib/querly/cli.rb
131
150
  - lib/querly/cli/console.rb
132
151
  - lib/querly/cli/formatter.rb
152
+ - lib/querly/cli/rules.rb
133
153
  - lib/querly/cli/test.rb
134
154
  - lib/querly/config.rb
135
155
  - lib/querly/node_pair.rb
@@ -138,11 +158,11 @@ files:
138
158
  - lib/querly/pattern/kind.rb
139
159
  - lib/querly/pattern/parser.rb
140
160
  - lib/querly/pattern/parser.y
161
+ - lib/querly/pp/cli.rb
141
162
  - lib/querly/preprocessor.rb
142
163
  - lib/querly/rule.rb
143
164
  - lib/querly/script.rb
144
165
  - lib/querly/script_enumerator.rb
145
- - lib/querly/tagging.rb
146
166
  - lib/querly/version.rb
147
167
  - querly.gemspec
148
168
  - sample.yaml
@@ -168,5 +188,5 @@ rubyforge_project:
168
188
  rubygems_version: 2.5.1
169
189
  signing_key:
170
190
  specification_version: 4
171
- summary: Query Method Calls from Ruby Programs
191
+ summary: Pattern Based Checking Tool for Ruby
172
192
  test_files: []
@@ -1,32 +0,0 @@
1
- module Querly
2
- class Tagging
3
- attr_reader :path_pattern
4
- attr_reader :tags_set
5
-
6
- def initialize(path_pattern:, tags_set:)
7
- @path_pattern = path_pattern
8
- @tags_set = tags_set
9
- end
10
-
11
- def applicable?(script)
12
- return true unless path_pattern
13
-
14
- pattern_components = path_pattern.split('/')
15
-
16
- script_path = if script.path.absolute?
17
- script.path
18
- else
19
- script.path.realpath
20
- end
21
- path_components = script_path.to_s.split(File::Separator)
22
-
23
- path_components.each_cons(pattern_components.size) do |slice|
24
- if slice == pattern_components
25
- return true
26
- end
27
- end
28
-
29
- false
30
- end
31
- end
32
- end